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 { type Model } from "@opencode-ai/llm"
|
||||||
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
|
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
|
||||||
import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
|
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 { Auth, type AnyRoute } from "@opencode-ai/llm/route"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { produce } from "immer"
|
import { produce } from "immer"
|
||||||
|
|
@ -139,8 +139,12 @@ export const fromCatalogModel = (
|
||||||
})
|
})
|
||||||
const key = apiKey(resolved, credential)
|
const key = apiKey(resolved, credential)
|
||||||
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
|
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(
|
return Effect.succeed(
|
||||||
withDefaults(resolved, OpenAIResponses.route)
|
withDefaults(resolved, route)
|
||||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||||
.model({ id: resolved.api.id }),
|
.model({ id: resolved.api.id }),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, expect } from "bun:test"
|
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 { LLMClient } from "@opencode-ai/llm/route"
|
||||||
import { DateTime, Effect } from "effect"
|
import { DateTime, Effect } from "effect"
|
||||||
import { Headers } from "effect/unstable/http"
|
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", () =>
|
it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue