Build OpenCode from source in Docker and remove broken session token queries
- Dockerfile: Build OpenCode CLI from source during Docker build instead of downloading from GitHub releases - Disabled GitHub Actions workflow that was failing to create releases - Removed getOpencodeSessionTokenUsage function that tried non-existent CLI commands (session info/usage/show) - Token tracking now relies on 3 layers: result extraction, streaming capture, and estimation
This commit is contained in:
26
Dockerfile
26
Dockerfile
@@ -1,10 +1,11 @@
|
|||||||
# Web-based PowerShell + SST OpenCode terminal
|
# Web-based PowerShell + SST OpenCode terminal
|
||||||
# Uses pre-built OpenCode CLI from GitHub releases
|
# Builds OpenCode CLI from source during Docker build
|
||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
ARG PWSH_VERSION=7.4.6
|
ARG PWSH_VERSION=7.4.6
|
||||||
ARG NODE_VERSION=20.18.1
|
ARG NODE_VERSION=20.18.1
|
||||||
ARG TTYD_VERSION=1.7.7
|
ARG TTYD_VERSION=1.7.7
|
||||||
|
ARG BUN_VERSION=1.3.8
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
@@ -23,6 +24,7 @@ RUN apt-get update \
|
|||||||
tini \
|
tini \
|
||||||
libicu-dev \
|
libicu-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
|
unzip \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Node.js
|
# Install Node.js
|
||||||
@@ -33,6 +35,10 @@ RUN curl -fsSL -o /tmp/node.tar.xz \
|
|||||||
&& ln -sf /usr/local/bin/npm /usr/bin/npm \
|
&& ln -sf /usr/local/bin/npm /usr/bin/npm \
|
||||||
&& rm -f /tmp/node.tar.xz
|
&& rm -f /tmp/node.tar.xz
|
||||||
|
|
||||||
|
# Install Bun
|
||||||
|
RUN npm install -g bun@${BUN_VERSION} \
|
||||||
|
&& bun --version
|
||||||
|
|
||||||
# Install PowerShell
|
# Install PowerShell
|
||||||
RUN curl -fsSL -o /tmp/powershell.tar.gz \
|
RUN curl -fsSL -o /tmp/powershell.tar.gz \
|
||||||
"https://github.com/PowerShell/PowerShell/releases/download/v${PWSH_VERSION}/powershell-${PWSH_VERSION}-linux-x64.tar.gz" \
|
"https://github.com/PowerShell/PowerShell/releases/download/v${PWSH_VERSION}/powershell-${PWSH_VERSION}-linux-x64.tar.gz" \
|
||||||
@@ -47,10 +53,20 @@ RUN curl -fsSL -o /usr/local/bin/ttyd \
|
|||||||
"https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" \
|
"https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" \
|
||||||
&& chmod +x /usr/local/bin/ttyd
|
&& chmod +x /usr/local/bin/ttyd
|
||||||
|
|
||||||
# Download OpenCode CLI from GitHub releases
|
# Build OpenCode CLI from source
|
||||||
# This will download the latest release - requires the release to exist first
|
COPY opencode /opt/opencode-src
|
||||||
RUN curl -fsSL -L -o /usr/local/bin/opencode \
|
WORKDIR /opt/opencode-src
|
||||||
"https://github.com/southseact-3d/shopify-ai-backup/releases/latest/download/opencode-linux-x64" \
|
|
||||||
|
# Configure git for any workspace dependencies that use git URLs
|
||||||
|
RUN git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
|
||||||
|
|
||||||
|
# Install dependencies and build OpenCode CLI
|
||||||
|
ENV HUSKY=0
|
||||||
|
RUN bun install 2>&1 \
|
||||||
|
&& bun run ./packages/opencode/script/build.ts --single
|
||||||
|
|
||||||
|
# Copy the built binary to /usr/local/bin
|
||||||
|
RUN cp /opt/opencode-src/packages/opencode/dist/opencode-linux-x64/bin/opencode /usr/local/bin/opencode \
|
||||||
&& chmod +x /usr/local/bin/opencode \
|
&& chmod +x /usr/local/bin/opencode \
|
||||||
&& opencode --version
|
&& opencode --version
|
||||||
|
|
||||||
|
|||||||
164
chat/server.js
164
chat/server.js
@@ -10483,19 +10483,6 @@ async function sendToOpencodeWithFallback({ session, model, content, message, cl
|
|||||||
log('✓ sendToOpencodeWithFallback: Using token usage captured during streaming', { tokensUsed, messageId: message?.id });
|
log('✓ sendToOpencodeWithFallback: Using token usage captured during streaming', { tokensUsed, messageId: message?.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try session query
|
|
||||||
if (!tokensUsed && opencodeSessionId && session?.workspaceDir) {
|
|
||||||
log('🔍 sendToOpencodeWithFallback: Attempting session token query', { sessionId: opencodeSessionId, messageId: message?.id });
|
|
||||||
tokensUsed = await getOpencodeSessionTokenUsage(opencodeSessionId, session.workspaceDir);
|
|
||||||
if (tokensUsed > 0) {
|
|
||||||
tokenSource = 'session';
|
|
||||||
tokenExtractionLog.push({ method: 'session_query', success: true, value: tokensUsed, context: 'sendToOpencodeWithFallback' });
|
|
||||||
log('✓ sendToOpencodeWithFallback: Got tokens from session', { tokensUsed, messageId: message?.id });
|
|
||||||
} else {
|
|
||||||
tokenExtractionLog.push({ method: 'session_query', success: false, reason: 'returned 0 tokens', context: 'sendToOpencodeWithFallback' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use estimation as last resort
|
// Use estimation as last resort
|
||||||
if (!tokensUsed) {
|
if (!tokensUsed) {
|
||||||
const inputTokens = estimateTokensFromMessages([messageContent], '');
|
const inputTokens = estimateTokensFromMessages([messageContent], '');
|
||||||
@@ -10923,24 +10910,6 @@ async function processMessage(sessionId, message) {
|
|||||||
log('✓ processMessage: Using token usage captured during streaming', { tokensUsed, messageId: message.id });
|
log('✓ processMessage: Using token usage captured during streaming', { tokensUsed, messageId: message.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try session query if still no tokens
|
|
||||||
if (!tokensUsed && session.opencodeSessionId && session.workspaceDir) {
|
|
||||||
try {
|
|
||||||
log('🔍 processMessage: Attempting session token query as fallback', { sessionId: session.opencodeSessionId, messageId: message.id });
|
|
||||||
tokensUsed = await getOpencodeSessionTokenUsage(session.opencodeSessionId, session.workspaceDir);
|
|
||||||
if (tokensUsed > 0) {
|
|
||||||
tokenSource = 'session';
|
|
||||||
tokenExtractionLog.push({ method: 'session_query', success: true, value: tokensUsed, context: 'processMessage fallback' });
|
|
||||||
log('✓ processMessage: Got tokens from session query', { tokensUsed, messageId: message.id });
|
|
||||||
} else {
|
|
||||||
tokenExtractionLog.push({ method: 'session_query', success: false, reason: 'returned 0 tokens', context: 'processMessage fallback' });
|
|
||||||
}
|
|
||||||
} catch (sessionErr) {
|
|
||||||
tokenExtractionLog.push({ method: 'session_query', success: false, error: String(sessionErr), context: 'processMessage fallback' });
|
|
||||||
log('✗ processMessage: Session token query failed', { error: String(sessionErr), messageId: message.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still no tokens, use estimation with detailed logging
|
// If still no tokens, use estimation with detailed logging
|
||||||
if (!tokensUsed) {
|
if (!tokensUsed) {
|
||||||
const inputTokens = estimateTokensFromMessages([message.content], '');
|
const inputTokens = estimateTokensFromMessages([message.content], '');
|
||||||
@@ -18158,135 +18127,10 @@ async function listOpencodeSessions(cwd) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOpencodeSessionTokenUsage(sessionId, cwd) {
|
// Note: Session token usage queries removed - OpenCode CLI doesn't support session info/usage/show commands.
|
||||||
if (!sessionId || !cwd) {
|
// Token tracking now relies on:
|
||||||
log('⚠️ getOpencodeSessionTokenUsage: Missing required parameters', { hasSessionId: !!sessionId, hasCwd: !!cwd });
|
// 1. Streaming capture during CLI execution (primary method)
|
||||||
return 0;
|
// 2. Estimation as fallback when streaming fails
|
||||||
}
|
|
||||||
|
|
||||||
const cliCommand = resolveCliCommand('opencode');
|
|
||||||
const candidates = [
|
|
||||||
['session', 'info', '--id', sessionId, '--json'],
|
|
||||||
['sessions', 'info', '--id', sessionId, '--json'],
|
|
||||||
['session', 'info', sessionId, '--json'],
|
|
||||||
['session', 'usage', '--id', sessionId, '--json'],
|
|
||||||
['session', 'show', '--id', sessionId, '--json'],
|
|
||||||
];
|
|
||||||
|
|
||||||
log('🔍 getOpencodeSessionTokenUsage: Starting session token query', {
|
|
||||||
sessionId,
|
|
||||||
cwd,
|
|
||||||
cliCommand,
|
|
||||||
candidateCount: candidates.length,
|
|
||||||
candidates: candidates.map(c => c.join(' '))
|
|
||||||
});
|
|
||||||
|
|
||||||
const attemptResults = [];
|
|
||||||
|
|
||||||
for (const args of candidates) {
|
|
||||||
const cmdStr = args.join(' ');
|
|
||||||
try {
|
|
||||||
log(` → Trying: ${cliCommand} ${cmdStr}`, { sessionId });
|
|
||||||
const { stdout, stderr } = await runCommand(cliCommand, args, { timeout: 10000, cwd });
|
|
||||||
|
|
||||||
const hasStdout = stdout && stdout.trim();
|
|
||||||
const hasStderr = stderr && stderr.trim();
|
|
||||||
|
|
||||||
log(` ← Response received`, {
|
|
||||||
args: cmdStr,
|
|
||||||
hasStdout,
|
|
||||||
hasStderr,
|
|
||||||
stdoutLength: stdout?.length || 0,
|
|
||||||
stderrLength: stderr?.length || 0,
|
|
||||||
stdoutSample: stdout?.substring(0, 300),
|
|
||||||
stderrSample: stderr?.substring(0, 200)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasStdout) {
|
|
||||||
// Try JSON parsing first
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(stdout);
|
|
||||||
log(' ✓ JSON parse successful', {
|
|
||||||
args: cmdStr,
|
|
||||||
parsedKeys: Object.keys(parsed),
|
|
||||||
hasUsage: !!parsed.usage,
|
|
||||||
hasTokens: !!parsed.tokens,
|
|
||||||
hasTokensUsed: !!parsed.tokensUsed,
|
|
||||||
hasSession: !!parsed.session
|
|
||||||
});
|
|
||||||
|
|
||||||
const extracted = extractTokenUsage(parsed) || extractTokenUsage(parsed.session) || null;
|
|
||||||
const tokens = extracted?.tokens || 0;
|
|
||||||
|
|
||||||
if (typeof tokens === 'number' && tokens > 0) {
|
|
||||||
log('✅ getOpencodeSessionTokenUsage: Successfully extracted tokens from JSON', {
|
|
||||||
sessionId,
|
|
||||||
tokens,
|
|
||||||
command: cmdStr,
|
|
||||||
extractionPath: extracted?.source || 'unknown'
|
|
||||||
});
|
|
||||||
attemptResults.push({ command: cmdStr, success: true, tokens, source: 'json' });
|
|
||||||
return tokens;
|
|
||||||
} else {
|
|
||||||
const reason = typeof tokens !== 'number' ? `tokens is ${typeof tokens}, not number` : 'tokens is 0 or negative';
|
|
||||||
log(' ✗ JSON parsed but no valid token count', { args: cmdStr, tokens, reason });
|
|
||||||
attemptResults.push({ command: cmdStr, success: false, reason, parsedTokens: tokens, source: 'json' });
|
|
||||||
}
|
|
||||||
} catch (jsonErr) {
|
|
||||||
log(' ✗ JSON parse failed, trying text parse', {
|
|
||||||
args: cmdStr,
|
|
||||||
error: jsonErr.message,
|
|
||||||
stdoutSample: stdout.substring(0, 200)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try to parse token count from text output
|
|
||||||
const tokenMatch = stdout.match(/total[_\s-]?tokens?\s*[:=]?\s*(\d+)/i) ||
|
|
||||||
stdout.match(/tokens?\s*[:=]?\s*(\d+)/i) ||
|
|
||||||
stdout.match(/token\s*count\s*[:=]?\s*(\d+)/i);
|
|
||||||
if (tokenMatch) {
|
|
||||||
const tokens = parseInt(tokenMatch[1], 10);
|
|
||||||
if (tokens > 0) {
|
|
||||||
log('✅ getOpencodeSessionTokenUsage: Successfully extracted tokens from text', {
|
|
||||||
sessionId,
|
|
||||||
tokens,
|
|
||||||
command: cmdStr,
|
|
||||||
pattern: tokenMatch[0]
|
|
||||||
});
|
|
||||||
attemptResults.push({ command: cmdStr, success: true, tokens, source: 'text', pattern: tokenMatch[0] });
|
|
||||||
return tokens;
|
|
||||||
} else {
|
|
||||||
log(' ✗ Text pattern matched but tokens <= 0', { args: cmdStr, tokens, pattern: tokenMatch[0] });
|
|
||||||
attemptResults.push({ command: cmdStr, success: false, reason: 'matched text pattern but tokens <= 0', parsedTokens: tokens, source: 'text' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log(' ✗ No text patterns matched', { args: cmdStr, stdoutSample: stdout.substring(0, 200) });
|
|
||||||
attemptResults.push({ command: cmdStr, success: false, reason: 'no text patterns matched', source: 'text' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const reason = !stdout ? 'no stdout' : 'stdout is empty';
|
|
||||||
log(' ✗ No stdout to parse', { args: cmdStr, reason, hasStderr });
|
|
||||||
attemptResults.push({ command: cmdStr, success: false, reason, stderr: stderr?.substring(0, 200) });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const errorDetails = {
|
|
||||||
message: err.message,
|
|
||||||
stderr: err.stderr?.substring(0, 200),
|
|
||||||
stdout: err.stdout?.substring(0, 200),
|
|
||||||
code: err.code
|
|
||||||
};
|
|
||||||
log(' ✗ Command execution failed', { args: cmdStr, error: errorDetails });
|
|
||||||
attemptResults.push({ command: cmdStr, success: false, error: errorDetails });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log('❌ getOpencodeSessionTokenUsage: All commands failed', {
|
|
||||||
sessionId,
|
|
||||||
totalAttempts: attemptResults.length,
|
|
||||||
attemptResults
|
|
||||||
});
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleGit(req, res, action) {
|
async function handleGit(req, res, action) {
|
||||||
// Validate git action
|
// Validate git action
|
||||||
|
|||||||
Reference in New Issue
Block a user