diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index f09818caa2e6..d75c01b647c2 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -394,27 +394,51 @@ export namespace Session { metadata: z.custom().optional(), }), (input) => { - const cachedInputTokens = input.usage.cachedInputTokens ?? 0 + const rawUsage = input.usage as Record + + // Extract anthropic/bedrock metadata usage (ZAI puts real tokens here) + const anthropicUsage = input.metadata?.["anthropic"]?.["usage"] as Record | undefined + const bedrockUsage = input.metadata?.["bedrock"]?.["usage"] as Record | undefined + + // Handle both underscore (ZAI) and camelCase (standard) field names + // Also handle nested usage object + const usage = (rawUsage.usage as Record) || rawUsage + + // CRITICAL: ZAI/Anthropic puts the real token counts in metadata.anthropic.usage + // The top-level usage.inputTokens is often 0, so we need to fallback to metadata + const inputTokens = + ((usage.input_tokens ?? usage.inputTokens) as number) || + ((anthropicUsage?.input_tokens ?? bedrockUsage?.input_tokens) as number) || + 0 + const outputTokens = + ((usage.output_tokens ?? usage.outputTokens) as number) || + ((anthropicUsage?.output_tokens ?? bedrockUsage?.output_tokens) as number) || + 0 + const cachedInputTokens = + ((usage.cache_read_input_tokens ?? usage.cachedInputTokens) as number) || + ((anthropicUsage?.cache_read_input_tokens ?? bedrockUsage?.cache_read_input_tokens) as number) || + 0 + const reasoningTokens = (usage.reasoning_tokens ?? usage.reasoningTokens ?? 0) as number + const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"]) - const adjustedInputTokens = excludesCachedTokens - ? (input.usage.inputTokens ?? 0) - : (input.usage.inputTokens ?? 0) - cachedInputTokens + const adjustedInputTokens = excludesCachedTokens ? inputTokens : inputTokens - cachedInputTokens + const safe = (value: number) => { if (!Number.isFinite(value)) return 0 return value } + const cacheWriteTokens = (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? + // @ts-expect-error + input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? + 0) as number + const tokens = { input: safe(adjustedInputTokens), - output: safe(input.usage.outputTokens ?? 0), - reasoning: safe(input.usage?.reasoningTokens ?? 0), + output: safe(outputTokens), + reasoning: safe(reasoningTokens), cache: { - write: safe( - (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? - // @ts-expect-error - input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? - 0) as number, - ), + write: safe(cacheWriteTokens), read: safe(cachedInputTokens), }, } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9152fc99bd81..49266f574411 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -268,6 +268,29 @@ export namespace SessionPrompt { } if (!lastUser) throw new Error("No user message found in stream. This should never happen.") + + // Get model info early for compaction check + const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID) + + // Check for context overflow BEFORE deciding to exit + // This ensures compaction triggers even when conversation is idle + // Skip if there's already a pending compaction task to avoid infinite loop + const hasPendingCompaction = tasks.some((t) => t.type === "compaction") + if ( + !hasPendingCompaction && + lastFinished && + lastFinished.summary !== true && + SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model: model.info }) + ) { + await SessionCompaction.create({ + sessionID, + agent: lastUser.agent, + model: lastUser.model, + auto: true, + }) + continue + } + if ( lastAssistant?.finish && !["tool-calls", "unknown"].includes(lastAssistant.finish) && @@ -287,7 +310,6 @@ export namespace SessionPrompt { history: msgs, }) - const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID) const task = tasks.pop() // pending subtask @@ -417,21 +439,6 @@ export namespace SessionPrompt { continue } - // context overflow, needs compaction - if ( - lastFinished && - lastFinished.summary !== true && - SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model: model.info }) - ) { - await SessionCompaction.create({ - sessionID, - agent: lastUser.agent, - model: lastUser.model, - auto: true, - }) - continue - } - // normal processing const agent = await Agent.get(lastUser.agent) msgs = insertReminders({ diff --git a/packages/opencode/src/session/prompt/glm.txt b/packages/opencode/src/session/prompt/glm.txt new file mode 100644 index 000000000000..62aab71e4cd1 --- /dev/null +++ b/packages/opencode/src/session/prompt/glm.txt @@ -0,0 +1,57 @@ +You are OpenCode, a powerful AI coding assistant optimized for software engineering tasks. + +Use the instructions below and the tools available to you to assist the user. + +IMPORTANT: Never generate or guess URLs unless they directly help with programming tasks. You may use URLs provided by the user. + +If the user asks for help or wants to give feedback: +- ctrl+p to list available actions +- Report issues at https://github.com/sst/opencode + +# Reasoning Approach +Think through problems systematically. Break complex tasks into logical steps before acting. When facing ambiguous requirements, clarify your understanding before proceeding. + +# Tone and Style +- No emojis unless requested +- Keep responses short and concise (CLI output) +- Use Github-flavored markdown (CommonMark, monospace font) +- Never use bash commands to communicate with the user +- NEVER create files unless necessary - prefer editing existing files + +# Professional Objectivity +Prioritize technical accuracy over validation. Focus on facts and problem-solving. Apply rigorous standards to all ideas and disagree when necessary. Objective guidance is more valuable than false agreement. + +# Security +Refuse to write or explain code that may be used maliciously. Analyze file/directory structure for purpose before working on code. + +# Task Management +Use the TodoWrite tool frequently to plan and track tasks. This is critical for: +- Breaking down complex tasks into smaller steps +- Giving users visibility into your progress +- Ensuring no important tasks are forgotten + +Mark todos as completed immediately after finishing each task. + +# Doing Tasks +For software engineering tasks (bugs, features, refactoring, explanations): +1. Understand the request and identify key components +2. Plan with TodoWrite for multi-step tasks +3. Research unfamiliar technologies with WebFetch when needed +4. Use the Task tool to explore codebase and gather context +5. Follow established patterns and conventions +6. Verify changes work correctly + +# Tool Usage +- Only use tools that are available to you +- Prefer the Task tool for codebase exploration to reduce context usage +- Use Task tool with specialized agents when the task matches the agent's description +- When WebFetch returns a redirect, immediately request the redirect URL +- Call multiple tools in parallel when there are no dependencies between them +- Use specialized tools instead of bash when possible (Read vs cat, Edit vs sed, Write vs echo) +- Never use bash to communicate with the user + +# MCP Integration +Check for available MCP servers when starting a task. Leverage them for additional context, tools, or capabilities. + +# Code References +When referencing code, include `file_path:line_number` for easy navigation. diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 399cad8cde55..fc80d783054e 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -17,6 +17,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt" import PROMPT_SUMMARIZE from "./prompt/summarize.txt" import PROMPT_TITLE from "./prompt/title.txt" import PROMPT_CODEX from "./prompt/codex.txt" +import PROMPT_GLM from "./prompt/glm.txt" export namespace SystemPrompt { export function header(providerID: string) { @@ -25,11 +26,14 @@ export namespace SystemPrompt { } export function provider(modelID: string) { - if (modelID.includes("gpt-5")) return [PROMPT_CODEX] - if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST] - if (modelID.includes("gemini-")) return [PROMPT_GEMINI] - if (modelID.includes("claude")) return [PROMPT_ANTHROPIC] - if (modelID.includes("polaris-alpha")) return [PROMPT_POLARIS] + const id = modelID.toLowerCase() + + if (id.includes("glm")) return [PROMPT_GLM] + if (id.includes("gpt-5")) return [PROMPT_CODEX] + if (id.includes("gpt-") || id.includes("o1") || id.includes("o3")) return [PROMPT_BEAST] + if (id.includes("gemini-")) return [PROMPT_GEMINI] + if (id.includes("claude")) return [PROMPT_ANTHROPIC] + if (id.includes("polaris-alpha")) return [PROMPT_POLARIS] return [PROMPT_ANTHROPIC_WITHOUT_TODO] }