fix(core): replay OpenAI reasoning statelessly
This commit is contained in:
parent
41f2705e72
commit
e174550f14
2 changed files with 84 additions and 3 deletions
|
|
@ -3,7 +3,7 @@ export * as SessionRunnerModel from "./model"
|
|||
import { type Model } from "@opencode-ai/llm"
|
||||
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
|
||||
import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
|
||||
import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
|
||||
import * as OpenAI from "@opencode-ai/llm/providers/openai"
|
||||
import { Auth, type AnyRoute } from "@opencode-ai/llm/route"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { produce } from "immer"
|
||||
|
|
@ -139,8 +139,12 @@ export const fromCatalogModel = (
|
|||
})
|
||||
const key = apiKey(resolved, credential)
|
||||
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
|
||||
const store = resolved.request.body.store
|
||||
const route = OpenAI.configure({
|
||||
providerOptions: typeof store === "boolean" ? { openai: { store } } : undefined,
|
||||
}).responses(resolved.api.id).route
|
||||
return Effect.succeed(
|
||||
withDefaults(resolved, OpenAIResponses.route)
|
||||
withDefaults(resolved, route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||
.model({ id: resolved.api.id }),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { LLM } from "@opencode-ai/llm"
|
||||
import { LLM, Message, ToolCallPart, ToolResultPart } from "@opencode-ai/llm"
|
||||
import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
|
||||
import { LLMClient } from "@opencode-ai/llm/route"
|
||||
import { DateTime, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
|
|
@ -72,6 +73,82 @@ describe("SessionRunnerModel", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("replays encrypted reasoning for stateless catalog OpenAI tool continuations", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = ModelV2.Info.make({
|
||||
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
||||
api: {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai",
|
||||
id: ModelV2.ID.make("custom-reasoning-model"),
|
||||
url: "https://openai.example/v1",
|
||||
},
|
||||
request: {
|
||||
headers: {},
|
||||
body: { include: ["reasoning.encrypted_content"] },
|
||||
},
|
||||
})
|
||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(catalog)
|
||||
const continuation = LLM.request({
|
||||
model: resolved,
|
||||
messages: [
|
||||
Message.user("Inspect the fixture."),
|
||||
Message.assistant([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "I should use the fixture tool.",
|
||||
providerMetadata: {
|
||||
openai: {
|
||||
itemId: "rs_fixture_1",
|
||||
reasoningEncryptedContent: "encrypted-fixture-state",
|
||||
},
|
||||
},
|
||||
},
|
||||
ToolCallPart.make({ id: "call_fixture_1", name: "inspect_fixture", input: { id: "fixture" } }),
|
||||
]),
|
||||
Message.tool(
|
||||
ToolResultPart.make({
|
||||
id: "call_fixture_1",
|
||||
name: "inspect_fixture",
|
||||
result: { status: "ok" },
|
||||
}),
|
||||
),
|
||||
],
|
||||
})
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(continuation)
|
||||
|
||||
expect(prepared.body.input.some((item) => "type" in item && item.type === "item_reference")).toBe(false)
|
||||
expect(prepared.body.store).toBe(false)
|
||||
expect(resolved.route.defaults.http?.body).toMatchObject({ include: ["reasoning.encrypted_content"] })
|
||||
expect(prepared.body.input).toContainEqual({
|
||||
type: "reasoning",
|
||||
id: "rs_fixture_1",
|
||||
summary: [{ type: "summary_text", text: "I should use the fixture tool." }],
|
||||
encrypted_content: "encrypted-fixture-state",
|
||||
})
|
||||
expect(prepared.body.input).toContainEqual({
|
||||
type: "function_call_output",
|
||||
call_id: "call_fixture_1",
|
||||
output: '{"status":"ok"}',
|
||||
})
|
||||
|
||||
const stateful = yield* SessionRunnerModel.fromCatalogModel(
|
||||
ModelV2.Info.make({
|
||||
...catalog,
|
||||
request: { ...catalog.request, body: { ...catalog.request.body, store: true } },
|
||||
}),
|
||||
)
|
||||
const statefulPrepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.updateRequest(continuation, {
|
||||
model: stateful,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(statefulPrepared.body.store).toBe(true)
|
||||
expect(statefulPrepared.body.input).toContainEqual({ type: "item_reference", id: "rs_fixture_1" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
|
||||
Effect.gen(function* () {
|
||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue