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
19 changes: 15 additions & 4 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1319,10 +1319,18 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las

const duration = createMemo(() => {
if (!final()) return 0
if (!props.message.time.completed) return 0
const user = messages().find((x) => x.role === "user" && x.id === props.message.parentID)
if (!user || !user.time) return 0
return props.message.time.completed - user.time.created
const ended = props.message.time.streamed ?? props.message.time.completed
if (!ended) return 0
const started = props.message.time.started ?? props.message.time.created
return ended - started
})

const tokensPerSecond = createMemo(() => {
const d = duration()
if (!d) return 0
const output = props.message.tokens.output
if (!output) return 0
return Math.round(output / (d / 1000))
})

const keybind = useKeybind()
Expand Down Expand Up @@ -1385,6 +1393,9 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
<Show when={duration()}>
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
</Show>
<Show when={tokensPerSecond()}>
<span style={{ fg: theme.textMuted }}> · {tokensPerSecond()} token/s</span>
</Show>
<Show when={props.message.error?.name === "MessageAbortedError"}>
<span style={{ fg: theme.textMuted }}> · interrupted</span>
</Show>
Expand Down
8 changes: 8 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,14 @@ export namespace Config {
.describe(
"Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
),
stripHeaders: z
.record(z.string(), z.array(z.string()))
.optional()
.describe(
"Strip specific values from comma-separated request headers before sending. " +
"Keys are header names, values are arrays of header values to remove. " +
'For example, {"anthropic-beta": ["structured-outputs-2025-11-13"]} removes that beta flag from the anthropic-beta header.',
),
})
.catchall(z.any())
.optional(),
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/provider/models-snapshot.ts

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,11 @@ export namespace Provider {

const customFetch = options["fetch"]
const chunkTimeout = options["chunkTimeout"]
const stripHeaders = options["stripHeaders"] as Record<string, string[]> | undefined
delete options["chunkTimeout"]
delete options["stripHeaders"]

const effectiveStripHeaders = stripHeaders ?? {}

options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
const fetchFn = customFetch ?? fetch
Expand All @@ -1357,6 +1361,25 @@ export namespace Provider {
const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
if (combined) opts.signal = combined

// Strip header values based on effectiveStripHeaders config
if (Object.keys(effectiveStripHeaders).length > 0) {
const headers = opts.headers as Record<string, string> | undefined
if (headers) {
for (const [headerName, valuesToStrip] of Object.entries(effectiveStripHeaders)) {
if (!headers[headerName]) continue
const filtered = headers[headerName]
.split(",")
.filter((v: string) => !valuesToStrip.includes(v.trim()))
.join(",")
if (filtered) {
headers[headerName] = filtered
} else {
delete headers[headerName]
}
}
}
}

// Strip openai itemId metadata following what codex does
if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") {
const body = JSON.parse(opts.body as string)
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ export namespace MessageV2 {
role: z.literal("assistant"),
time: z.object({
created: z.number(),
started: z.number().optional(),
streamed: z.number().optional(),
completed: z.number().optional(),
}),
error: z
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export namespace SessionProcessor {
switch (value.type) {
case "start":
yield* status.set(ctx.sessionID, { type: "busy" })
ctx.assistantMessage.time.started = Date.now()
yield* session.updateMessage(ctx.assistantMessage)
return

case "reasoning-start":
Expand Down Expand Up @@ -356,6 +358,8 @@ export namespace SessionProcessor {
return

case "finish":
ctx.assistantMessage.time.streamed = Date.now()
yield* session.updateMessage(ctx.assistantMessage)
return

default:
Expand Down
Loading
Loading