mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6208a8c50 |
+1
-1
@@ -132,7 +132,7 @@ _Avoid_: Response envelope
|
||||
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
|
||||
- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
|
||||
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn.
|
||||
- **Native Continuation Metadata** remains in durable history. Provider-turn projection includes it only for a successful exact originating provider/model match; failed turns and incompatible models omit opaque metadata, while non-empty visible reasoning lowers to ordinary assistant text after a model switch. This conservative relation may widen only when recorded provider tests establish compatibility.
|
||||
- **Native Continuation Metadata** remains in durable history. Provider-turn projection includes it only for a successful exact originating provider/model match; failed turns and incompatible models omit opaque metadata. Visible reasoning remains canonical until target-protocol lowering: OpenAI Chat-compatible targets replay it as `reasoning_content`, while protocols that require signed native reasoning lower unsigned reasoning to ordinary assistant text.
|
||||
- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding.
|
||||
- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing.
|
||||
- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`.
|
||||
|
||||
@@ -74,17 +74,13 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
|
||||
const content = message.content.flatMap((item): ContentPart[] => {
|
||||
if (item.type === "text") return [{ type: "text", text: item.text }]
|
||||
if (item.type === "reasoning")
|
||||
return sameModel
|
||||
? [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: item.text,
|
||||
providerMetadata: reuseProviderMetadata ? item.providerMetadata : undefined,
|
||||
},
|
||||
]
|
||||
: item.text.length > 0
|
||||
? [{ type: "text", text: item.text }]
|
||||
: []
|
||||
return [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: item.text,
|
||||
providerMetadata: reuseProviderMetadata ? item.providerMetadata : undefined,
|
||||
},
|
||||
]
|
||||
const call = toolCall(item, reuseProviderMetadata ? item.provider?.metadata : undefined)
|
||||
if (item.provider?.executed !== true) return [call]
|
||||
const result = toolResult(
|
||||
|
||||
@@ -457,7 +457,7 @@ Recent work
|
||||
)
|
||||
|
||||
expect(messages[0]?.content).toEqual([
|
||||
{ type: "text", text: "Visible thought" },
|
||||
{ type: "reasoning", text: "Visible thought", providerMetadata: undefined },
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "hosted-old-model",
|
||||
|
||||
@@ -447,10 +447,15 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
continue
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata)
|
||||
if (!signature) {
|
||||
content.push({ type: "text", text: part.text })
|
||||
continue
|
||||
}
|
||||
content.push({
|
||||
type: "thinking",
|
||||
thinking: part.text,
|
||||
signature: part.encrypted ?? signatureFromMetadata(part.providerMetadata),
|
||||
signature,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -348,9 +348,14 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
|
||||
continue
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
const signature = reasoningSignature(part)
|
||||
if (!signature) {
|
||||
content.push({ text: part.text })
|
||||
continue
|
||||
}
|
||||
content.push({
|
||||
reasoningContent: {
|
||||
reasoningText: { text: part.text, signature: reasoningSignature(part) },
|
||||
reasoningText: { text: part.text, signature },
|
||||
},
|
||||
})
|
||||
continue
|
||||
|
||||
@@ -236,7 +236,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
|
||||
continue
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
parts.push({ text: part.text, thought: true, thoughtSignature: thoughtSignature(part.providerMetadata) })
|
||||
const signature = thoughtSignature(part.providerMetadata)
|
||||
parts.push(signature ? { text: part.text, thought: true, thoughtSignature: signature } : { text: part.text })
|
||||
continue
|
||||
}
|
||||
if (part.type === "tool-call") {
|
||||
|
||||
@@ -383,9 +383,12 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
|
||||
continue
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
flushText()
|
||||
const reasoning = lowerReasoning(part)
|
||||
if (!reasoning) continue
|
||||
if (!reasoning) {
|
||||
content.push({ type: "text", text: part.text })
|
||||
continue
|
||||
}
|
||||
flushText()
|
||||
if (store !== false) {
|
||||
if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id })
|
||||
reasoningReferences.add(reasoning.id)
|
||||
|
||||
@@ -362,6 +362,18 @@ describe("Anthropic Messages route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers unsigned foreign reasoning to assistant text", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({ model, messages: [Message.assistant({ type: "reasoning", text: "foreign thought" })] }),
|
||||
)
|
||||
|
||||
expect(prepared.body).toMatchObject({
|
||||
messages: [{ role: "assistant", content: [{ type: "text", text: "foreign thought" }] }],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("parses text, reasoning, and usage stream fixtures", () =>
|
||||
Effect.gen(function* () {
|
||||
const body = sseEvents(
|
||||
|
||||
@@ -355,6 +355,20 @@ describe("Bedrock Converse route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers unsigned foreign reasoning to assistant text", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [Message.assistant({ type: "reasoning", text: "foreign thought" })],
|
||||
cache: "none",
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.messages).toEqual([{ role: "assistant", content: [{ text: "foreign thought" }] }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("emits provider-error for throttlingException", () =>
|
||||
Effect.gen(function* () {
|
||||
const body = eventStreamBody(
|
||||
|
||||
@@ -431,6 +431,16 @@ describe("Gemini route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers unsigned foreign reasoning to assistant text", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
LLM.request({ model, messages: [Message.assistant({ type: "reasoning", text: "foreign thought" })] }),
|
||||
)
|
||||
|
||||
expect(prepared.body.contents).toEqual([{ role: "model", parts: [{ text: "foreign thought" }] }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("emits streamed tool calls and maps finish reason", () =>
|
||||
Effect.gen(function* () {
|
||||
const body = sseEvents({
|
||||
|
||||
@@ -818,6 +818,34 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers foreign reasoning without continuation state to assistant text", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{ type: "text", text: "before" },
|
||||
{ type: "reasoning", text: "foreign thought" },
|
||||
{ type: "text", text: "after" },
|
||||
]),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.input).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "output_text", text: "before" },
|
||||
{ type: "output_text", text: "foreign thought" },
|
||||
{ type: "output_text", text: "after" },
|
||||
],
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("streams each reasoning summary part as a separate block", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
|
||||
@@ -110,7 +110,7 @@ const messages = (input: readonly ModelMessage[]) => {
|
||||
Message.make({
|
||||
role: message.role,
|
||||
content: content(message.content),
|
||||
native: isRecord(message.providerOptions) ? { providerOptions: message.providerOptions } : undefined,
|
||||
native: isRecord(message.providerOptions) ? message.providerOptions : undefined,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
@@ -243,6 +243,10 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
|
||||
|
||||
if (msg.info.role === "assistant") {
|
||||
const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}`
|
||||
const replaysForeignReasoning =
|
||||
model.api.npm === "@ai-sdk/openai-compatible" &&
|
||||
typeof model.capabilities.interleaved === "object" &&
|
||||
model.capabilities.interleaved.field === "reasoning_content"
|
||||
const media: Array<{ mime: string; url: string; filename?: string }> = []
|
||||
|
||||
if (
|
||||
@@ -360,7 +364,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
|
||||
})
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
if (differentModel) {
|
||||
if (differentModel && !replaysForeignReasoning) {
|
||||
if (part.text.trim().length > 0)
|
||||
assistantMessage.parts.push({
|
||||
type: "text",
|
||||
@@ -371,7 +375,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
|
||||
assistantMessage.parts.push({
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
providerMetadata: part.metadata,
|
||||
...(differentModel ? {} : { providerMetadata: part.metadata }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +332,39 @@ describe("session.llm-native.request", () => {
|
||||
])
|
||||
})
|
||||
|
||||
it.effect("preserves OpenAI-compatible reasoning fields through the native adapter", () =>
|
||||
Effect.gen(function* () {
|
||||
const compatible = {
|
||||
...baseModel,
|
||||
providerID: ProviderV2.ID.make("moonshotai"),
|
||||
api: {
|
||||
id: "kimi-k2.5",
|
||||
url: "https://api.moonshot.test/v1",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
},
|
||||
capabilities: {
|
||||
...baseModel.capabilities,
|
||||
interleaved: { field: "reasoning_content" as const },
|
||||
},
|
||||
}
|
||||
const prepared = yield* prepareNativeRequest({
|
||||
model: compatible,
|
||||
apiKey: "test-key",
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "answer" }],
|
||||
providerOptions: { openaiCompatible: { reasoning_content: "thinking" } },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(prepared.body).toMatchObject({
|
||||
messages: [{ role: "assistant", content: "answer", reasoning_content: "thinking" }],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
test("selects native request routes for provider packages", () => {
|
||||
const openai = LLMNative.model({
|
||||
model: { ...baseModel, api: { ...baseModel.api, url: "", npm: "@ai-sdk/openai" } },
|
||||
|
||||
@@ -684,6 +684,42 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("preserves foreign reasoning for OpenAI-compatible reasoning fields", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
const compatible = {
|
||||
...model,
|
||||
api: { ...model.api, npm: "@ai-sdk/openai-compatible" },
|
||||
capabilities: { ...model.capabilities, reasoning: true, interleaved: { field: "reasoning_content" } },
|
||||
} satisfies Provider.Model
|
||||
const input: SessionV1.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-user", undefined, { providerID: "other", modelID: "other" }),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID, "a1"),
|
||||
type: "reasoning",
|
||||
text: "thinking",
|
||||
metadata: { anthropic: { signature: "foreign" } },
|
||||
time: { start: 0 },
|
||||
},
|
||||
{
|
||||
...basePart(assistantID, "a2"),
|
||||
type: "text",
|
||||
text: "answer",
|
||||
},
|
||||
] as SessionV1.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(ProviderTransform.message(await MessageV2.toModelMessages(input, compatible), compatible, {})).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "answer" }],
|
||||
providerOptions: { openaiCompatible: { reasoning_content: "thinking" } },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("replaces compacted tool output with placeholder", async () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ SessionExecution.resume(sessionID)
|
||||
|
||||
The local runner issues one explicit `llm.stream(request)` per provider turn, projects each complete local tool call durably before eagerly starting its structured child execution, awaits every started tool fiber after provider-stream closure, and reloads projected history once before continuation. Promoting any new user input resets the selected agent's configured provider-turn allowance; multiple steers promoted at one boundary reset it once. Tool settlement events carry the owning assistant message ID because provider-local call IDs may repeat across turns. Before assembling a provider request, the runner durably fails any local tool still projected as `running` from a previous process with `Tool execution interrupted`; abandoned side effects are never silently replayed.
|
||||
|
||||
Projected hosted tools preserve call-side and settlement-side provider metadata separately so settlement and interruption recovery cannot erase continuation identifiers. Provider-native reasoning and provider metadata replay only while the historical assistant model matches the selected continuation model; after a model switch, visible reasoning text remains ordinary assistant text and provider-native metadata is omitted.
|
||||
Projected hosted tools preserve call-side and settlement-side provider metadata separately so settlement and interruption recovery cannot erase continuation identifiers. Provider-native metadata replays only while the historical assistant model matches the selected continuation model. After a model switch, visible reasoning remains canonical without opaque metadata until the target protocol either replays it in a compatible reasoning field or lowers unsigned reasoning to ordinary assistant text.
|
||||
|
||||
## Context Epochs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user