chore: merge dev
This commit is contained in:
@@ -33,6 +33,7 @@ import { TestConfig } from "../fixture/config"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { LLMEvent, Usage } from "@opencode-ai/llm"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -47,7 +48,7 @@ const summary = Layer.succeed(
|
||||
|
||||
const ref = {
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("test-model"),
|
||||
modelID: ModelV2.ID.make("test-model"),
|
||||
}
|
||||
|
||||
const usage = (input: ConstructorParameters<typeof Usage>[0]) => new Usage(input)
|
||||
|
||||
@@ -16,6 +16,7 @@ import { provideInstance, provideTmpdirInstance, testInstanceStoreLayer, tmpdirS
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { TestConfig } from "../fixture/config"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer))
|
||||
|
||||
@@ -77,7 +78,7 @@ function loaded(filepath: string): SessionV1.WithParts[] {
|
||||
agent: "build",
|
||||
model: {
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
modelID: ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
|
||||
modelID: ModelV2.ID.make("claude-sonnet-4-20250514"),
|
||||
},
|
||||
},
|
||||
parts: [
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { HttpRecorder, Redactor } from "@opencode-ai/http-recorder"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { tool, type ModelMessage, type JSONValue } from "ai"
|
||||
import { Effect, Layer, Option, Schema, Stream } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import path from "node:path"
|
||||
import z from "zod"
|
||||
import { Auth } from "@/auth"
|
||||
@@ -28,6 +25,7 @@ import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures/recordings")
|
||||
|
||||
@@ -280,23 +278,21 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) {
|
||||
Layer.provide(Plugin.defaultLayer),
|
||||
Layer.provide(ModelsDev.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
)
|
||||
// Only the HTTP client is recorded; RequestExecutor and the opencode LLM stack remain real.
|
||||
const recordedHttp = HttpRecorder.cassetteLayer(scenario.cassette, {
|
||||
directory: FIXTURES_DIR,
|
||||
mode: shouldRecord ? "record" : "replay",
|
||||
metadata: {
|
||||
provider: scenario.providerID,
|
||||
protocol: scenario.protocol,
|
||||
route: scenario.protocol,
|
||||
tags: scenario.tags,
|
||||
},
|
||||
redactor: recordingRedactor,
|
||||
})
|
||||
const recordedClient = LLMClient.layer.pipe(
|
||||
Layer.provide(Layer.mergeAll(RequestExecutor.layer, WebSocketExecutor.layer)),
|
||||
Layer.provide(
|
||||
HttpRecorder.recordingLayer(scenario.cassette, {
|
||||
mode: shouldRecord ? "record" : "replay",
|
||||
metadata: {
|
||||
provider: scenario.providerID,
|
||||
protocol: scenario.protocol,
|
||||
route: scenario.protocol,
|
||||
tags: scenario.tags,
|
||||
},
|
||||
redactor: recordingRedactor,
|
||||
}).pipe(Layer.provide(FetchHttpClient.layer)),
|
||||
),
|
||||
Layer.provide(Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer)),
|
||||
)
|
||||
|
||||
return Layer.mergeAll(
|
||||
@@ -307,9 +303,6 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) {
|
||||
Layer.provide(provider),
|
||||
Layer.provide(Plugin.defaultLayer),
|
||||
Layer.provide(recordedClient),
|
||||
Layer.provide(
|
||||
HttpRecorder.Cassette.fileSystem({ directory: FIXTURES_DIR }).pipe(Layer.provide(NodeFileSystem.layer)),
|
||||
),
|
||||
Layer.provide(RuntimeFlags.layer({ experimentalNativeLlm: true })),
|
||||
),
|
||||
)
|
||||
@@ -376,7 +369,7 @@ const driveToolLoop = (scenario: RecordedScenario) =>
|
||||
|
||||
const stableID = scenario.stableID ?? scenario.providerID
|
||||
const sessionID = SessionID.make(`session-recorded-${stableID}-loop`)
|
||||
const modelID = ProviderV2.ModelID.make(model.id)
|
||||
const modelID = ModelV2.ID.make(model.id)
|
||||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route"
|
||||
import { LLMEvent, ToolFailure } from "@opencode-ai/llm"
|
||||
import { LLMClient, RequestExecutor, WebSocketExecutor, type LLMClientShape } from "@opencode-ai/llm/route"
|
||||
import { jsonSchema, tool, type ModelMessage, type Tool } from "ai"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { Effect, Fiber, Layer, Stream } from "effect"
|
||||
import { LLMNative } from "@/session/llm/native-request"
|
||||
import { LLMNativeRuntime } from "@/session/llm/native-runtime"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
@@ -10,9 +10,10 @@ import type { Provider } from "@/provider/provider"
|
||||
import { OAUTH_DUMMY_KEY } from "@/auth"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const baseModel: Provider.Model = {
|
||||
id: ProviderV2.ModelID.make("gpt-5-mini"),
|
||||
id: ModelV2.ID.make("gpt-5-mini"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
api: {
|
||||
id: "gpt-5-mini",
|
||||
@@ -535,6 +536,66 @@ describe("session.llm-native.request", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("emits native tool calls before overlapping local settlements complete", () =>
|
||||
Effect.gen(function* () {
|
||||
const observed: string[] = []
|
||||
const started: string[] = []
|
||||
let release: (() => void) | undefined
|
||||
let notifyStarted: (() => void) | undefined
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
const bothStarted = new Promise<void>((resolve) => {
|
||||
notifyStarted = resolve
|
||||
})
|
||||
const lookup = {
|
||||
description: "Lookup data",
|
||||
inputSchema: jsonSchema({ type: "object" }),
|
||||
execute: async (_args: unknown, options: { toolCallId: string }) => {
|
||||
started.push(options.toolCallId)
|
||||
if (started.length === 2) notifyStarted?.()
|
||||
await gate
|
||||
return { output: options.toolCallId }
|
||||
},
|
||||
} satisfies Tool
|
||||
const llmClient = {
|
||||
prepare: () => Effect.die("unused"),
|
||||
stream: () =>
|
||||
Stream.fromIterable([
|
||||
LLMEvent.toolCall({ id: "call-1", name: "lookup", input: {} }),
|
||||
LLMEvent.toolCall({ id: "call-2", name: "lookup", input: {} }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
]),
|
||||
generate: () => Effect.die("unused"),
|
||||
} as LLMClientShape
|
||||
const native = LLMNativeRuntime.stream({
|
||||
model: baseModel,
|
||||
provider: providerInfo,
|
||||
auth: undefined,
|
||||
llmClient,
|
||||
messages: [],
|
||||
tools: { lookup },
|
||||
headers: {},
|
||||
abort: new AbortController().signal,
|
||||
})
|
||||
expect(native.type).toBe("supported")
|
||||
if (native.type === "unsupported") throw new Error(native.reason)
|
||||
|
||||
const fiber = yield* native.stream.pipe(
|
||||
Stream.runForEach((event) => Effect.sync(() => observed.push(event.type))),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* Effect.promise(() => bothStarted)
|
||||
|
||||
expect(started).toEqual(["call-1", "call-2"])
|
||||
expect(observed).toEqual(["tool-call", "tool-call", "finish"])
|
||||
|
||||
release?.()
|
||||
yield* Fiber.join(fiber)
|
||||
expect(observed).toEqual(["tool-call", "tool-call", "finish", "tool-result", "tool-result"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("compiles through the native OpenAI Responses route", () =>
|
||||
expectOpenAIResponsesRequest({
|
||||
history: [storedSession.user("hello")],
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Permission } from "@/permission"
|
||||
import { LLMAISDK } from "@/session/llm/ai-sdk"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
type ConfigModel = NonNullable<NonNullable<ConfigV1.Info["provider"]>[string]["models"]>[string]
|
||||
|
||||
@@ -768,7 +769,7 @@ describe("session.llm.stream", () => {
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderV2.ID.make(vivgridFixture.providerID),
|
||||
ProviderV2.ModelID.make(fixture.model.id),
|
||||
ModelV2.ID.make(fixture.model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-1")
|
||||
const agent = {
|
||||
@@ -842,7 +843,7 @@ describe("session.llm.stream", () => {
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderV2.ID.make(alibabaQwenFixture.providerID),
|
||||
ProviderV2.ModelID.make(fixture.model.id),
|
||||
ModelV2.ID.make(fixture.model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-service-abort")
|
||||
const agent = {
|
||||
@@ -910,7 +911,7 @@ describe("session.llm.stream", () => {
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderV2.ID.make(alibabaQwenFixture.providerID),
|
||||
ProviderV2.ModelID.make(fixture.model.id),
|
||||
ModelV2.ID.make(fixture.model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-tools")
|
||||
const agent = {
|
||||
@@ -1013,7 +1014,7 @@ describe("session.llm.stream", () => {
|
||||
]
|
||||
const request = waitRequest("/responses", createEventResponse(responseChunks, true))
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ModelV2.ID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-2")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1118,7 +1119,7 @@ describe("session.llm.stream", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ModelV2.ID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-native-flag-off")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1188,7 +1189,7 @@ describe("session.llm.stream", () => {
|
||||
]
|
||||
const request = waitRequest("/responses", createEventResponse(chunks, true))
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ModelV2.ID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-native")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1272,7 +1273,7 @@ describe("session.llm.stream", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ModelV2.ID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-native-injected-tool")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1360,7 +1361,7 @@ describe("session.llm.stream", () => {
|
||||
const request = waitRequest("/responses", createEventResponse(chunks, true))
|
||||
let executed: unknown
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ModelV2.ID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-native-tool")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1486,7 +1487,7 @@ describe("session.llm.stream", () => {
|
||||
),
|
||||
).toString("base64")}`
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ModelV2.ID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-data-url")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1575,7 +1576,7 @@ describe("session.llm.stream", () => {
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderV2.ID.make(minimaxFixture.providerID),
|
||||
ProviderV2.ModelID.make(model.id),
|
||||
ModelV2.ID.make(model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-3")
|
||||
const agent = {
|
||||
@@ -1593,7 +1594,7 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderV2.ID.make("minimax"), modelID: ProviderV2.ModelID.make("MiniMax-M2.5") },
|
||||
model: { providerID: ProviderV2.ID.make("minimax"), modelID: ModelV2.ID.make("MiniMax-M2.5") },
|
||||
} satisfies SessionV1.User
|
||||
|
||||
yield* drain({
|
||||
@@ -1672,7 +1673,7 @@ describe("session.llm.stream", () => {
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderV2.ID.make("anthropic"),
|
||||
ProviderV2.ModelID.make(model.id),
|
||||
ModelV2.ID.make(model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-anthropic-tools")
|
||||
const agent = {
|
||||
@@ -1874,7 +1875,7 @@ describe("session.llm.stream", () => {
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderV2.ID.make(geminiFixture.providerID),
|
||||
ProviderV2.ModelID.make(model.id),
|
||||
ModelV2.ID.make(model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-4")
|
||||
const agent = {
|
||||
|
||||
@@ -8,11 +8,12 @@ import type { Provider } from "@/provider/provider"
|
||||
import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
import { Question } from "../../src/question"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const sessionID = SessionID.make("session")
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const model: Provider.Model = {
|
||||
id: ProviderV2.ModelID.make("test-model"),
|
||||
id: ModelV2.ID.make("test-model"),
|
||||
providerID,
|
||||
api: {
|
||||
id: "test-model",
|
||||
@@ -67,7 +68,7 @@ function userInfo(id: string): SessionV1.User {
|
||||
role: "user",
|
||||
time: { created: 0 },
|
||||
agent: "user",
|
||||
model: { providerID, modelID: ProviderV2.ModelID.make("test") },
|
||||
model: { providerID, modelID: ModelV2.ID.make("test") },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as SessionV1.User
|
||||
@@ -413,7 +414,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("preserves jpeg tool-result media for anthropic models", async () => {
|
||||
const anthropicModel: Provider.Model = {
|
||||
...model,
|
||||
id: ProviderV2.ModelID.make("anthropic/claude-opus-4-7"),
|
||||
id: ModelV2.ID.make("anthropic/claude-opus-4-7"),
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
api: {
|
||||
id: "claude-opus-4-7-20250805",
|
||||
@@ -496,7 +497,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("moves bedrock pdf tool-result media into a separate user message", async () => {
|
||||
const bedrockModel: Provider.Model = {
|
||||
...model,
|
||||
id: ProviderV2.ModelID.make("amazon-bedrock/anthropic.claude-sonnet-4-6"),
|
||||
id: ModelV2.ID.make("amazon-bedrock/anthropic.claude-sonnet-4-6"),
|
||||
providerID: ProviderV2.ID.make("amazon-bedrock"),
|
||||
api: {
|
||||
id: "anthropic.claude-sonnet-4-6",
|
||||
@@ -1044,7 +1045,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const assistantID = "m-assistant"
|
||||
const openrouterModel: Provider.Model = {
|
||||
...model,
|
||||
id: ProviderV2.ModelID.make("deepseek/deepseek-v4-pro"),
|
||||
id: ModelV2.ID.make("deepseek/deepseek-v4-pro"),
|
||||
providerID: ProviderV2.ID.make("openrouter"),
|
||||
api: {
|
||||
id: "deepseek/deepseek-v4-pro",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { NotFoundError } from "@/storage/storage"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -98,7 +99,7 @@ const addAssistant = Effect.fn("Test.addAssistant")(function* (
|
||||
role: "assistant",
|
||||
time: { created: Date.now() },
|
||||
parentID,
|
||||
modelID: ProviderV2.ModelID.make("test"),
|
||||
modelID: ModelV2.ID.make("test"),
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
mode: "",
|
||||
agent: "default",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { expect } from "bun:test"
|
||||
import { tool } from "ai"
|
||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import type { Agent } from "../../src/agent/agent"
|
||||
@@ -25,11 +25,14 @@ import { SessionSummary } from "../../src/session/summary"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { provideTmpdirServer } from "../fixture/fixture"
|
||||
import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { raw, reply, TestLLMServer } from "../lib/llm-server"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { LLMEvent } from "@opencode-ai/llm"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -44,7 +47,7 @@ const summary = Layer.succeed(
|
||||
|
||||
const ref = {
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("test-model"),
|
||||
modelID: ModelV2.ID.make("test-model"),
|
||||
}
|
||||
|
||||
const cfg = {
|
||||
@@ -198,6 +201,58 @@ const env = Layer.mergeAll(
|
||||
|
||||
const it = testEffect(env)
|
||||
|
||||
const providerErrorLLM = Layer.succeed(
|
||||
LLM.Service,
|
||||
LLM.Service.of({
|
||||
stream: () =>
|
||||
Stream.make(
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolInputStart({ id: "call-1", name: "lookup" }),
|
||||
LLMEvent.toolInputEnd({ id: "call-1", name: "lookup" }),
|
||||
LLMEvent.toolCall({ id: "call-1", name: "lookup", input: {}, providerExecuted: true }),
|
||||
LLMEvent.toolResult({
|
||||
id: "call-1",
|
||||
name: "lookup",
|
||||
result: { type: "error", value: "provider boom" },
|
||||
providerExecuted: true,
|
||||
}),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
),
|
||||
}),
|
||||
)
|
||||
const providerErrorEnv = SessionProcessor.layer.pipe(
|
||||
Layer.provide(summary),
|
||||
Layer.provide(Image.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
|
||||
Layer.provide(providerErrorLLM),
|
||||
Layer.provideMerge(deps),
|
||||
)
|
||||
const itProviderError = testEffect(providerErrorEnv)
|
||||
|
||||
const fragmentFailureLLM = Layer.succeed(
|
||||
LLM.Service,
|
||||
LLM.Service.of({
|
||||
stream: () =>
|
||||
Stream.make(
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.reasoningStart({ id: "reasoning-1" }),
|
||||
LLMEvent.reasoningDelta({ id: "reasoning-1", text: "thinking" }),
|
||||
LLMEvent.textStart({ id: "text-1" }),
|
||||
LLMEvent.textDelta({ id: "text-1", text: "partial" }),
|
||||
LLMEvent.providerError({ message: "provider boom" }),
|
||||
),
|
||||
}),
|
||||
)
|
||||
const fragmentFailureEnv = SessionProcessor.layer.pipe(
|
||||
Layer.provide(summary),
|
||||
Layer.provide(Image.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
|
||||
Layer.provide(fragmentFailureLLM),
|
||||
Layer.provideMerge(deps),
|
||||
)
|
||||
const itFragmentFailure = testEffect(fragmentFailureEnv)
|
||||
|
||||
const boot = Effect.fn("test.boot")(function* () {
|
||||
const processors = yield* SessionProcessor.Service
|
||||
const session = yield* Session.Service
|
||||
@@ -936,3 +991,111 @@ it.live("session.processor effect tests mark interruptions aborted without manua
|
||||
{ config: (url) => providerCfg(url) },
|
||||
),
|
||||
)
|
||||
|
||||
itProviderError.live("session.processor effect tests fail provider-executed error results", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
const { processors, session, provider } = yield* boot()
|
||||
const events = yield* EventV2Bridge.Service
|
||||
|
||||
const chat = yield* session.create({})
|
||||
const parent = yield* user(chat.id, "provider tool error")
|
||||
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
|
||||
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
||||
const settlements: Array<typeof SessionEvent.Tool.Failed.Type> = []
|
||||
const off = yield* events.listen((event) => {
|
||||
if (event.type === SessionEvent.Tool.Failed.type)
|
||||
settlements.push(event as typeof SessionEvent.Tool.Failed.Type)
|
||||
return Effect.void
|
||||
})
|
||||
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
|
||||
|
||||
yield* handle.process({
|
||||
user: {
|
||||
id: parent.id,
|
||||
sessionID: chat.id,
|
||||
role: "user",
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies SessionV1.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
system: [],
|
||||
messages: [{ role: "user", content: "provider tool error" }],
|
||||
tools: {},
|
||||
})
|
||||
yield* off
|
||||
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
const call = parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
|
||||
expect(call?.state.status).toBe("error")
|
||||
if (call?.state.status === "error") expect(call.state.error).toBe("provider boom")
|
||||
expect(settlements).toHaveLength(1)
|
||||
expect(settlements[0]?.data).toMatchObject({
|
||||
callID: "call-1",
|
||||
error: { type: "unknown", message: "provider boom" },
|
||||
result: { type: "error", value: "provider boom" },
|
||||
provider: { executed: true },
|
||||
})
|
||||
}),
|
||||
{ config: cfg },
|
||||
),
|
||||
)
|
||||
|
||||
itFragmentFailure.live("session.processor effect tests flush partial v2 fragments before step failure", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
const { processors, session, provider } = yield* boot()
|
||||
const events = yield* EventV2Bridge.Service
|
||||
|
||||
const chat = yield* session.create({})
|
||||
const parent = yield* user(chat.id, "provider failure")
|
||||
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
|
||||
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
||||
const seen: string[] = []
|
||||
let text: string | undefined
|
||||
let reasoning: string | undefined
|
||||
const off = yield* events.listen((event) => {
|
||||
seen.push(event.type)
|
||||
if (event.type === SessionEvent.Text.Ended.type)
|
||||
text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text
|
||||
if (event.type === SessionEvent.Reasoning.Ended.type)
|
||||
reasoning = (event.data as typeof SessionEvent.Reasoning.Ended.data.Type).text
|
||||
return Effect.void
|
||||
})
|
||||
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
|
||||
|
||||
expect(
|
||||
yield* handle.process({
|
||||
user: {
|
||||
id: parent.id,
|
||||
sessionID: chat.id,
|
||||
role: "user",
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies SessionV1.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
system: [],
|
||||
messages: [{ role: "user", content: "provider failure" }],
|
||||
tools: {},
|
||||
}),
|
||||
).toBe("stop")
|
||||
yield* off
|
||||
|
||||
const failed = seen.indexOf(SessionEvent.Step.Failed.type)
|
||||
expect(failed).toBeGreaterThan(-1)
|
||||
expect(seen.indexOf(SessionEvent.Text.Ended.type)).toBeLessThan(failed)
|
||||
expect(seen.indexOf(SessionEvent.Reasoning.Ended.type)).toBeLessThan(failed)
|
||||
expect(text).toBe("partial")
|
||||
expect(reasoning).toBe("thinking")
|
||||
}),
|
||||
{ config: cfg },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -57,6 +57,7 @@ import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
|
||||
import { reply, TestLLMServer } from "../lib/llm-server"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -71,7 +72,7 @@ const summary = Layer.succeed(
|
||||
|
||||
const ref = {
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("test-model"),
|
||||
modelID: ModelV2.ID.make("test-model"),
|
||||
}
|
||||
|
||||
function withSh<A, E, R>(fx: () => Effect.Effect<A, E, R>) {
|
||||
@@ -320,10 +321,9 @@ const useServerConfig = Effect.fn("test.useServerConfig")(function* (config: (ur
|
||||
return { dir, llm }
|
||||
})
|
||||
|
||||
// Wait for a session's runner to enter a busy state. SessionStatus is flipped to
|
||||
// "busy" inside Runner.startShell's modifyEffect at the same moment the runner
|
||||
// is registered, so this is a deterministic readiness signal — cancel can't
|
||||
// no-op once we observe it.
|
||||
// Wait for a session's runner to enter a busy state. SessionStatus is flipped
|
||||
// inside Runner.startShell's serialized transition, so cancel can't no-op once
|
||||
// we observe it.
|
||||
const waitForBusy = (sessionID: SessionID, duration: Duration.Input = "2 seconds") =>
|
||||
pollWithTimeout(
|
||||
Effect.gen(function* () {
|
||||
@@ -760,7 +760,7 @@ it.instance("failed subtask preserves metadata on error tool state", () =>
|
||||
expect(tool.state.metadata?.sessionId).toBeDefined()
|
||||
expect(tool.state.metadata?.model).toEqual({
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("missing-model"),
|
||||
modelID: ModelV2.ID.make("missing-model"),
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -2206,7 +2206,7 @@ noLLMServer.instance(
|
||||
const other = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
model: { providerID: ProviderV2.ID.make("opencode"), modelID: ProviderV2.ModelID.make("kimi-k2.5-free") },
|
||||
model: { providerID: ProviderV2.ID.make("opencode"), modelID: ModelV2.ID.make("kimi-k2.5-free") },
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
@@ -2222,7 +2222,7 @@ noLLMServer.instance(
|
||||
if (match.info.role !== "user") throw new Error("expected user message")
|
||||
expect(match.info.model).toEqual({
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("test-model"),
|
||||
modelID: ModelV2.ID.make("test-model"),
|
||||
variant: "xhigh",
|
||||
})
|
||||
expect(match.info.model.variant).toBe("xhigh")
|
||||
|
||||
@@ -14,6 +14,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -33,7 +34,7 @@ const user = Effect.fn("test.user")(function* (sessionID: SessionID, agent = "de
|
||||
role: "user" as const,
|
||||
sessionID,
|
||||
agent,
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: ProviderV2.ModelID.make("gpt-4") },
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: ModelV2.ID.make("gpt-4") },
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
})
|
||||
@@ -49,7 +50,7 @@ const assistant = Effect.fn("test.assistant")(function* (sessionID: SessionID, p
|
||||
path: { cwd: dir, root: dir },
|
||||
cost: 0,
|
||||
tokens: { output: 0, input: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
modelID: ModelV2.ID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
parentID,
|
||||
time: { created: Date.now() },
|
||||
@@ -117,7 +118,7 @@ describe("revert + compact workflow", () => {
|
||||
agent: "default",
|
||||
model: {
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
modelID: ModelV2.ID.make("gpt-4"),
|
||||
},
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -149,7 +150,7 @@ describe("revert + compact workflow", () => {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
modelID: ModelV2.ID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
parentID: userMsg1.id,
|
||||
time: {
|
||||
@@ -174,7 +175,7 @@ describe("revert + compact workflow", () => {
|
||||
agent: "default",
|
||||
model: {
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
modelID: ModelV2.ID.make("gpt-4"),
|
||||
},
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -206,7 +207,7 @@ describe("revert + compact workflow", () => {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
modelID: ModelV2.ID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
parentID: userMsg2.id,
|
||||
time: {
|
||||
@@ -279,7 +280,7 @@ describe("revert + compact workflow", () => {
|
||||
agent: "default",
|
||||
model: {
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
modelID: ModelV2.ID.make("gpt-4"),
|
||||
},
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -311,7 +312,7 @@ describe("revert + compact workflow", () => {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
modelID: ModelV2.ID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
parentID: userMsg.id,
|
||||
time: {
|
||||
|
||||
Reference in New Issue
Block a user