Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions packages/opencode/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,27 +394,51 @@ export namespace Session {
metadata: z.custom<ProviderMetadata>().optional(),
}),
(input) => {
const cachedInputTokens = input.usage.cachedInputTokens ?? 0
const rawUsage = input.usage as Record<string, unknown>

// Extract anthropic/bedrock metadata usage (ZAI puts real tokens here)
const anthropicUsage = input.metadata?.["anthropic"]?.["usage"] as Record<string, unknown> | undefined
const bedrockUsage = input.metadata?.["bedrock"]?.["usage"] as Record<string, unknown> | undefined

// Handle both underscore (ZAI) and camelCase (standard) field names
// Also handle nested usage object
const usage = (rawUsage.usage as Record<string, unknown>) || 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),
},
}
Expand Down
39 changes: 23 additions & 16 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) &&
Expand All @@ -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
Expand Down Expand Up @@ -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({
Expand Down
57 changes: 57 additions & 0 deletions packages/opencode/src/session/prompt/glm.txt
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 9 additions & 5 deletions packages/opencode/src/session/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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]
}

Expand Down