| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003 |
- import { PermissionV1 } from "@kirincode-ai/core/v1/permission"
- import { ConfigV1 } from "@kirincode-ai/core/v1/config/config"
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"
- import { SessionV1 } from "@kirincode-ai/core/v1/session"
- import path from "path"
- import { tool, type ModelMessage } from "ai"
- import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect"
- import { InstanceRef } from "../../src/effect/instance-ref"
- import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
- import z from "zod"
- import { LLM } from "../../src/session/llm"
- import { LLMClient, RequestExecutor } from "@kirincode-ai/llm/route"
- import { Provider } from "@/provider/provider"
- import { ProviderTransform } from "@/provider/transform"
- import { ModelsDev } from "@kirincode-ai/core/models-dev"
- import { testEffect } from "../lib/effect"
- import type { Agent } from "../../src/agent/agent"
- import { MessageV2 } from "../../src/session/message-v2"
- import { SessionID, MessageID } from "../../src/session/schema"
- import { RuntimeFlags } from "@/effect/runtime-flags"
- import { Permission } from "@/permission"
- import { LLMAISDK } from "@/session/llm/ai-sdk"
- import { Session as SessionNs } from "@/session/session"
- import { ProviderV2 } from "@kirincode-ai/core/provider"
- import { ModelV2 } from "@kirincode-ai/core/model"
- import { AppNodeBuilder } from "@kirincode-ai/core/effect/app-node-builder"
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import { LayerNodePlatform } from "@kirincode-ai/core/effect/app-node-platform"
- type ConfigModel = NonNullable<NonNullable<ConfigV1.Info["provider"]>[string]["models"]>[string]
- const openAIConfig = (model: ModelsDev.Provider["models"][string], baseURL: string): Partial<ConfigV1.Info> => {
- const { experimental: _experimental, ...configModel } = model
- return {
- enabled_providers: ["openai"],
- provider: {
- openai: {
- name: "OpenAI",
- env: ["OPENAI_API_KEY"],
- npm: "@ai-sdk/openai",
- api: "https://api.openai.com/v1",
- models: {
- [model.id]: JSON.parse(JSON.stringify(configModel)) as ConfigModel,
- },
- options: {
- apiKey: "test-openai-key",
- baseURL,
- },
- },
- },
- }
- }
- const it = testEffect(AppNodeBuilder.build(LayerNode.group([LLM.node, Provider.node])))
- // LLM.stream returns a Stream, not an Effect, so we can't use the serviceUse proxy.
- const drain = (input: LLM.StreamInput) => LLM.Service.use((svc) => svc.stream(input).pipe(Stream.runDrain))
- // drainWith builds an isolated runtime so custom replacements fully own LLM and
- // its transitive deps.
- const drainWith = (layer: Layer.Layer<LLM.Service>, input: LLM.StreamInput) =>
- Effect.gen(function* () {
- const ctx = yield* InstanceRef
- if (!ctx) return yield* Effect.die("InstanceRef not provided")
- return yield* Effect.promise(() =>
- Effect.runPromise(
- LLM.Service.use((svc) => svc.stream(input).pipe(Stream.runDrain)).pipe(
- Effect.provide(layer),
- Effect.provideService(InstanceRef, ctx),
- ),
- ),
- )
- })
- function llmLayerWithExecutor(
- options: {
- executor?: Layer.Layer<RequestExecutor.Service>
- flags?: Partial<RuntimeFlags.Info>
- } = {},
- ) {
- return AppNodeBuilder.build(LLM.node, [
- [RuntimeFlags.node, RuntimeFlags.layer(options.flags)],
- ...(options.executor ? ([[LayerNodePlatform.requestExecutor, options.executor]] as const) : []),
- ])
- }
- describe("session.llm.hasToolCalls", () => {
- test("returns false for empty messages array", () => {
- expect(LLM.hasToolCalls([])).toBe(false)
- })
- test("returns false for messages with only text content", () => {
- const messages: ModelMessage[] = [
- {
- role: "user",
- content: [{ type: "text", text: "Hello" }],
- },
- {
- role: "assistant",
- content: [{ type: "text", text: "Hi there" }],
- },
- ]
- expect(LLM.hasToolCalls(messages)).toBe(false)
- })
- test("returns true when messages contain tool-call", () => {
- const messages = [
- {
- role: "user",
- content: [{ type: "text", text: "Run a command" }],
- },
- {
- role: "assistant",
- content: [
- {
- type: "tool-call",
- toolCallId: "call-123",
- toolName: "bash",
- },
- ],
- },
- ] as ModelMessage[]
- expect(LLM.hasToolCalls(messages)).toBe(true)
- })
- test("returns true when messages contain tool-result", () => {
- const messages = [
- {
- role: "tool",
- content: [
- {
- type: "tool-result",
- toolCallId: "call-123",
- toolName: "bash",
- },
- ],
- },
- ] as ModelMessage[]
- expect(LLM.hasToolCalls(messages)).toBe(true)
- })
- test("returns false for messages with string content", () => {
- const messages: ModelMessage[] = [
- {
- role: "user",
- content: "Hello world",
- },
- {
- role: "assistant",
- content: "Hi there",
- },
- ]
- expect(LLM.hasToolCalls(messages)).toBe(false)
- })
- test("returns true when tool-call is mixed with text content", () => {
- const messages = [
- {
- role: "assistant",
- content: [
- { type: "text", text: "Let me run that command" },
- {
- type: "tool-call",
- toolCallId: "call-456",
- toolName: "read",
- },
- ],
- },
- ] as ModelMessage[]
- expect(LLM.hasToolCalls(messages)).toBe(true)
- })
- })
- describe("session.llm.ai-sdk adapter", () => {
- type AISDKAdapterEvent = Parameters<typeof LLMAISDK.toLLMEvents>[1]
- const adapt = (events: ReadonlyArray<AISDKAdapterEvent>) => {
- const state = LLMAISDK.adapterState()
- return Effect.runPromise(
- Effect.forEach(events, (event) => LLMAISDK.toLLMEvents(state, event)).pipe(Effect.map((items) => items.flat())),
- )
- }
- // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- tests defensive adapter branches outside AI SDK's current typed surface
- const uncheckedAdapterEvent = (input: unknown) => input as AISDKAdapterEvent
- test("maps AI SDK stream chunks without losing session-visible fields", async () => {
- const metadata = { openai: { itemID: "item-1" } }
- const events = await adapt([
- { type: "start" },
- { type: "start-step", request: {}, warnings: [] },
- { type: "text-start", id: "text-1", providerMetadata: metadata },
- { type: "text-delta", id: "text-1", text: "Hel", providerMetadata: { openai: { delta: 1 } } },
- { type: "text-delta", id: "text-1", text: "lo", providerMetadata: { openai: { delta: 2 } } },
- { type: "text-end", id: "text-1", providerMetadata: { openai: { done: true } } },
- { type: "reasoning-start", id: "reasoning-1", providerMetadata: metadata },
- { type: "reasoning-delta", id: "reasoning-1", text: "Think", providerMetadata: { openai: { delta: 3 } } },
- { type: "reasoning-end", id: "reasoning-1", providerMetadata: { openai: { done: true } } },
- { type: "tool-input-start", id: "call-1", toolName: "lookup", providerMetadata: metadata },
- { type: "tool-input-delta", id: "call-1", delta: '{"query":' },
- { type: "tool-input-delta", id: "call-1", delta: '"weather"}' },
- { type: "tool-input-end", id: "call-1", providerMetadata: { openai: { inputDone: true } } },
- {
- type: "tool-call",
- toolCallId: "call-1",
- toolName: "lookup",
- input: { query: "weather" },
- providerExecuted: true,
- providerMetadata: { openai: { called: true } },
- },
- {
- type: "tool-result",
- toolCallId: "call-1",
- toolName: "lookup",
- input: { query: "weather" },
- output: { title: "Lookup", output: "sunny", metadata: { ok: true } },
- providerExecuted: true,
- providerMetadata: { openai: { result: true } },
- },
- {
- type: "finish-step",
- response: { id: "response-1", timestamp: new Date(0), modelId: "gpt-test" },
- finishReason: "other",
- rawFinishReason: "other",
- usage: {
- inputTokens: 10,
- outputTokens: 5,
- totalTokens: 15,
- inputTokenDetails: { noCacheTokens: 5, cacheReadTokens: 3, cacheWriteTokens: 2 },
- outputTokenDetails: { textTokens: 4, reasoningTokens: 1 },
- },
- providerMetadata: { openai: { step: true } },
- },
- {
- type: "finish",
- finishReason: "other",
- rawFinishReason: "other",
- totalUsage: {
- inputTokens: 11,
- outputTokens: 6,
- totalTokens: 17,
- cachedInputTokens: 4,
- reasoningTokens: 2,
- inputTokenDetails: { noCacheTokens: 7, cacheReadTokens: 4, cacheWriteTokens: undefined },
- outputTokenDetails: { textTokens: 4, reasoningTokens: 2 },
- },
- },
- ])
- expect(events).toMatchObject([
- { type: "step-start", index: 0 },
- { type: "text-start", id: "text-1", providerMetadata: metadata },
- { type: "text-delta", id: "text-1", text: "Hel", providerMetadata: { openai: { delta: 1 } } },
- { type: "text-delta", id: "text-1", text: "lo", providerMetadata: { openai: { delta: 2 } } },
- { type: "text-end", id: "text-1", providerMetadata: { openai: { done: true } } },
- { type: "reasoning-start", id: "reasoning-1", providerMetadata: metadata },
- { type: "reasoning-delta", id: "reasoning-1", text: "Think", providerMetadata: { openai: { delta: 3 } } },
- { type: "reasoning-end", id: "reasoning-1", providerMetadata: { openai: { done: true } } },
- { type: "tool-input-start", id: "call-1", name: "lookup", providerMetadata: metadata },
- { type: "tool-input-delta", id: "call-1", name: "lookup", text: '{"query":' },
- { type: "tool-input-delta", id: "call-1", name: "lookup", text: '"weather"}' },
- { type: "tool-input-end", id: "call-1", name: "lookup", providerMetadata: { openai: { inputDone: true } } },
- {
- type: "tool-call",
- id: "call-1",
- name: "lookup",
- input: { query: "weather" },
- providerExecuted: true,
- providerMetadata: { openai: { called: true } },
- },
- {
- type: "tool-result",
- id: "call-1",
- name: "lookup",
- result: { type: "json", value: { title: "Lookup", output: "sunny", metadata: { ok: true } } },
- providerExecuted: true,
- providerMetadata: { openai: { result: true } },
- },
- {
- type: "step-finish",
- index: 0,
- reason: "unknown",
- usage: {
- inputTokens: 10,
- outputTokens: 5,
- totalTokens: 15,
- reasoningTokens: 1,
- cacheReadInputTokens: 3,
- cacheWriteInputTokens: 2,
- },
- providerMetadata: { openai: { step: true } },
- },
- {
- type: "finish",
- reason: "unknown",
- usage: {
- inputTokens: 11,
- outputTokens: 6,
- totalTokens: 17,
- reasoningTokens: 2,
- cacheReadInputTokens: 4,
- },
- },
- ])
- })
- test("creates stable block ids when AI SDK omits them", async () => {
- const events = await adapt([
- uncheckedAdapterEvent({ type: "text-delta", text: "implicit text" }),
- uncheckedAdapterEvent({ type: "text-end" }),
- uncheckedAdapterEvent({ type: "reasoning-delta", text: "implicit reasoning" }),
- uncheckedAdapterEvent({ type: "reasoning-end" }),
- ])
- expect(events).toMatchObject([
- { type: "text-delta", id: "text-0", text: "implicit text" },
- { type: "text-end", id: "text-0" },
- { type: "reasoning-delta", id: "reasoning-0", text: "implicit reasoning" },
- { type: "reasoning-end", id: "reasoning-0" },
- ])
- })
- test("explicitly ignores non-session-visible AI SDK chunks", async () => {
- expect(
- await adapt([
- uncheckedAdapterEvent({ type: "abort" }),
- uncheckedAdapterEvent({ type: "source" }),
- uncheckedAdapterEvent({ type: "file" }),
- uncheckedAdapterEvent({ type: "raw" }),
- uncheckedAdapterEvent({ type: "tool-output-denied" }),
- uncheckedAdapterEvent({ type: "tool-approval-request" }),
- ]),
- ).toEqual([])
- })
- test("preserves tool-error cause", async () => {
- const error = new PermissionV1.RejectedError()
- const events = await Effect.runPromise(
- LLMAISDK.toLLMEvents(LLMAISDK.adapterState(), {
- type: "tool-error",
- toolCallId: "call_123",
- toolName: "bash",
- input: {},
- error,
- }),
- )
- expect(events).toHaveLength(1)
- expect(events[0]).toMatchObject({
- type: "tool-error",
- id: "call_123",
- name: "bash",
- message: error.message,
- error,
- })
- })
- test("emits undefined usage when every AI SDK usage field is missing", async () => {
- // If every numeric field is undefined the translator should signal "no usage info"
- // by emitting undefined, not by polluting the event with usage: {}. Downstream cost
- // telemetry distinguishes "missing" from "zero," so emitting an empty object causes
- // false positives ("usage was tracked, just empty") instead of correct nulls.
- const events = await adapt([
- {
- type: "finish-step",
- response: { id: "response-1", timestamp: new Date(0), modelId: "gpt-test" },
- finishReason: "stop",
- rawFinishReason: "stop",
- providerMetadata: undefined,
- usage: {
- inputTokens: undefined,
- outputTokens: undefined,
- totalTokens: undefined,
- reasoningTokens: undefined,
- cachedInputTokens: undefined,
- inputTokenDetails: { noCacheTokens: undefined, cacheReadTokens: undefined, cacheWriteTokens: undefined },
- outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
- },
- },
- ])
- expect(events).toHaveLength(1)
- const stepFinish = events[0]
- if (stepFinish.type !== "step-finish") throw new Error("expected step-finish")
- expect(stepFinish.usage).toBeUndefined()
- })
- test("reuses adapter state cleanly across streams once finish has fired", async () => {
- // adapterState() is meant to be per-stream, but the only thing finish currently clears
- // is toolNames — step, text counters, and the current text/reasoning IDs all leak
- // forward. A caller that reuses a state across two streams sees text-1/reasoning-1/
- // step index 1 on the second stream's first events. The test pins the intended
- // contract: after finish, the same state can be reused and starts fresh.
- const state = LLMAISDK.adapterState()
- const run = (events: ReadonlyArray<AISDKAdapterEvent>) =>
- Effect.runPromise(
- Effect.forEach(events, (event) => LLMAISDK.toLLMEvents(state, event)).pipe(Effect.map((items) => items.flat())),
- )
- await run([
- { type: "start-step", request: {}, warnings: [] },
- uncheckedAdapterEvent({ type: "text-delta", text: "first" }),
- uncheckedAdapterEvent({ type: "text-end" }),
- uncheckedAdapterEvent({ type: "reasoning-delta", text: "first reasoning" }),
- uncheckedAdapterEvent({ type: "reasoning-end" }),
- {
- type: "finish-step",
- response: { id: "r1", timestamp: new Date(0), modelId: "gpt-test" },
- finishReason: "stop",
- rawFinishReason: "stop",
- providerMetadata: undefined,
- usage: {
- inputTokens: 1,
- outputTokens: 1,
- totalTokens: 2,
- inputTokenDetails: { noCacheTokens: undefined, cacheReadTokens: undefined, cacheWriteTokens: undefined },
- outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
- },
- },
- {
- type: "finish",
- finishReason: "stop",
- rawFinishReason: "stop",
- totalUsage: {
- inputTokens: 1,
- outputTokens: 1,
- totalTokens: 2,
- inputTokenDetails: { noCacheTokens: undefined, cacheReadTokens: undefined, cacheWriteTokens: undefined },
- outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
- },
- },
- ])
- const secondStream = await run([
- { type: "start-step", request: {}, warnings: [] },
- uncheckedAdapterEvent({ type: "text-delta", text: "second" }),
- uncheckedAdapterEvent({ type: "text-end" }),
- uncheckedAdapterEvent({ type: "reasoning-delta", text: "second reasoning" }),
- uncheckedAdapterEvent({ type: "reasoning-end" }),
- ])
- expect(secondStream).toMatchObject([
- { type: "step-start", index: 0 },
- { type: "text-delta", id: "text-0", text: "second" },
- { type: "text-end", id: "text-0" },
- { type: "reasoning-delta", id: "reasoning-0", text: "second reasoning" },
- { type: "reasoning-end", id: "reasoning-0" },
- ])
- })
- // Anthropic emits cache write counts in providerMetadata.anthropic.cacheCreationInputTokens
- // rather than usage.inputTokenDetails.cacheWriteTokens. Session.getUsage falls back to the
- // metadata path — but only if the adapter preserves providerMetadata on step-finish.
- test("preserves providerMetadata on step-finish so Anthropic cache writes survive getUsage", async () => {
- const events = await adapt([
- {
- type: "finish-step",
- response: { id: "msg_test", timestamp: new Date(0), modelId: "claude-3-5-sonnet" },
- finishReason: "stop",
- rawFinishReason: "stop",
- // Anthropic's AI SDK shape: cacheWriteTokens is NOT in usage, it arrives via providerMetadata.
- usage: {
- inputTokens: 1000,
- outputTokens: 500,
- totalTokens: 1500,
- inputTokenDetails: { noCacheTokens: 800, cacheReadTokens: 200, cacheWriteTokens: undefined },
- outputTokenDetails: { textTokens: 500, reasoningTokens: undefined },
- },
- providerMetadata: { anthropic: { cacheCreationInputTokens: 300 } },
- },
- ])
- expect(events).toHaveLength(1)
- const stepFinish = events[0]
- if (stepFinish.type !== "step-finish") throw new Error("expected step-finish")
- expect(stepFinish.providerMetadata).toEqual({ anthropic: { cacheCreationInputTokens: 300 } })
- expect(stepFinish.usage?.cacheWriteInputTokens).toBeUndefined()
- expect(stepFinish.usage?.cacheReadInputTokens).toBe(200)
- // End-to-end: with the metadata preserved, getUsage extracts cache.write from the fallback path.
- const result = SessionNs.getUsage({
- model: {
- id: "claude-3-5-sonnet",
- providerID: "anthropic",
- name: "Claude",
- limit: { context: 200_000, output: 8_000 },
- cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
- capabilities: {
- toolcall: true,
- attachment: false,
- reasoning: false,
- temperature: true,
- input: { text: true, image: false, audio: false, video: false },
- output: { text: true, image: false, audio: false, video: false },
- },
- api: { npm: "@ai-sdk/anthropic" },
- options: {},
- } as never,
- usage: stepFinish.usage!,
- metadata: stepFinish.providerMetadata,
- })
- expect(result.tokens.cache.write).toBe(300)
- expect(result.tokens.cache.read).toBe(200)
- })
- test("captures Copilot billed usage from raw Anthropic message deltas per step", async () => {
- const events = await adapt([
- uncheckedAdapterEvent({
- type: "raw",
- rawValue: {
- type: "message_delta",
- copilot_usage: { total_nano_aiu: 4_473_525_000 },
- },
- }),
- {
- type: "finish-step",
- response: { id: "msg_test", timestamp: new Date(0), modelId: "claude-sonnet-4.6" },
- finishReason: "stop",
- rawFinishReason: "end_turn",
- usage: {
- inputTokens: 11_774,
- outputTokens: 39,
- totalTokens: 11_813,
- inputTokenDetails: { noCacheTokens: 3, cacheReadTokens: 0, cacheWriteTokens: 11_771 },
- outputTokenDetails: { textTokens: 39, reasoningTokens: undefined },
- },
- providerMetadata: { anthropic: { cacheCreationInputTokens: 11_771 } },
- },
- {
- type: "finish-step",
- response: { id: "msg_follow_up", timestamp: new Date(0), modelId: "claude-sonnet-4.6" },
- finishReason: "stop",
- rawFinishReason: "end_turn",
- usage: {
- inputTokens: 1,
- outputTokens: 1,
- totalTokens: 2,
- inputTokenDetails: { noCacheTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 },
- outputTokenDetails: { textTokens: 1, reasoningTokens: undefined },
- },
- providerMetadata: { anthropic: {} },
- },
- ])
- expect(events[0]).toMatchObject({
- type: "step-finish",
- providerMetadata: {
- anthropic: { cacheCreationInputTokens: 11_771 },
- copilot: { totalNanoAiu: 4_473_525_000 },
- },
- })
- expect(events[1]).toMatchObject({ type: "step-finish", providerMetadata: { anthropic: {} } })
- if (events[1].type !== "step-finish") throw new Error("expected step-finish")
- expect(events[1].providerMetadata?.copilot).toBeUndefined()
- })
- })
- type Capture = {
- url: URL
- headers: Headers
- body: Record<string, unknown>
- }
- const state = {
- server: null as ReturnType<typeof Bun.serve> | null,
- queue: [] as Array<{
- path: string
- response: Response | ((req: Request, capture: Capture) => Response)
- resolve: (value: Capture) => void
- }>,
- }
- function deferred<T>() {
- const result = {} as { promise: Promise<T>; resolve: (value: T) => void }
- result.promise = new Promise((resolve) => {
- result.resolve = resolve
- })
- return result
- }
- function waitRequest(pathname: string, response: Response) {
- const pending = deferred<Capture>()
- state.queue.push({ path: pathname, response, resolve: pending.resolve })
- return pending.promise
- }
- function timeout(ms: number) {
- return new Promise<never>((_, reject) => {
- setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms)
- })
- }
- function waitStreamingRequest(pathname: string) {
- const request = deferred<Capture>()
- const requestAborted = deferred<void>()
- const responseCanceled = deferred<void>()
- const encoder = new TextEncoder()
- state.queue.push({
- path: pathname,
- resolve: request.resolve,
- response(req: Request) {
- req.signal.addEventListener("abort", () => requestAborted.resolve(), { once: true })
- return new Response(
- new ReadableStream<Uint8Array>({
- start(controller) {
- controller.enqueue(
- encoder.encode(
- [
- `data: ${JSON.stringify({
- id: "chatcmpl-abort",
- object: "chat.completion.chunk",
- choices: [{ delta: { role: "assistant" } }],
- })}`,
- ].join("\n\n") + "\n\n",
- ),
- )
- },
- cancel() {
- responseCanceled.resolve()
- },
- }),
- {
- status: 200,
- headers: { "Content-Type": "text/event-stream" },
- },
- )
- },
- })
- return {
- request: request.promise,
- requestAborted: requestAborted.promise,
- responseCanceled: responseCanceled.promise,
- }
- }
- beforeAll(() => {
- state.server = Bun.serve({
- port: 0,
- async fetch(req) {
- const next = state.queue.shift()
- if (!next) {
- return new Response("unexpected request", { status: 500 })
- }
- const url = new URL(req.url)
- const body = (await req.json()) as Record<string, unknown>
- next.resolve({ url, headers: req.headers, body })
- if (!url.pathname.endsWith(next.path)) {
- return new Response("not found", { status: 404 })
- }
- return typeof next.response === "function"
- ? next.response(req, { url, headers: req.headers, body })
- : next.response
- },
- })
- })
- beforeEach(() => {
- state.queue.length = 0
- })
- afterAll(() => {
- void state.server?.stop()
- })
- function createChatStream(text: string) {
- const payload =
- [
- `data: ${JSON.stringify({
- id: "chatcmpl-1",
- object: "chat.completion.chunk",
- choices: [{ delta: { role: "assistant" } }],
- })}`,
- `data: ${JSON.stringify({
- id: "chatcmpl-1",
- object: "chat.completion.chunk",
- choices: [{ delta: { content: text } }],
- })}`,
- `data: ${JSON.stringify({
- id: "chatcmpl-1",
- object: "chat.completion.chunk",
- choices: [{ delta: {}, finish_reason: "stop" }],
- })}`,
- "data: [DONE]",
- ].join("\n\n") + "\n\n"
- const encoder = new TextEncoder()
- return new ReadableStream<Uint8Array>({
- start(controller) {
- controller.enqueue(encoder.encode(payload))
- controller.close()
- },
- })
- }
- const MODELS_FIXTURE = JSON.parse(
- await Bun.file(path.join(import.meta.dir, "../tool/fixtures/models-api.json")).text(),
- ) as Record<string, ModelsDev.Provider>
- function loadFixture(providerID: string, modelID: string) {
- const provider = MODELS_FIXTURE[providerID]
- if (!provider) throw new Error(`Missing provider in fixture: ${providerID}`)
- const model = provider.models[modelID]
- if (!model) throw new Error(`Missing model in fixture: ${modelID}`)
- return { provider, model }
- }
- function configModel(model: ModelsDev.Model) {
- return {
- id: model.id,
- name: model.name,
- family: model.family,
- release_date: model.release_date,
- attachment: model.attachment,
- reasoning: model.reasoning,
- temperature: model.temperature,
- tool_call: model.tool_call,
- interleaved: model.interleaved,
- cost: model.cost ? { ...model.cost, tiers: undefined } : undefined,
- limit: model.limit,
- modalities: model.modalities,
- status: model.status,
- provider: model.provider,
- }
- }
- function createEventStream(chunks: unknown[], includeDone = false) {
- const lines = chunks.map((chunk) => `data: ${typeof chunk === "string" ? chunk : JSON.stringify(chunk)}`)
- if (includeDone) {
- lines.push("data: [DONE]")
- }
- const payload = lines.join("\n\n") + "\n\n"
- const encoder = new TextEncoder()
- return new ReadableStream<Uint8Array>({
- start(controller) {
- controller.enqueue(encoder.encode(payload))
- controller.close()
- },
- })
- }
- function createEventResponse(chunks: unknown[], includeDone = false) {
- return new Response(createEventStream(chunks, includeDone), {
- status: 200,
- headers: { "Content-Type": "text/event-stream" },
- })
- }
- describe("session.llm.stream", () => {
- const vivgridFixture = { providerID: "vivgrid", modelID: "gemini-3.1-pro-preview" }
- it.instance(
- "sends temperature, tokens, and reasoning options for openai-compatible models",
- () =>
- Effect.gen(function* () {
- const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID)
- const request = waitRequest(
- "/chat/completions",
- new Response(createChatStream("Hello"), {
- status: 200,
- headers: { "Content-Type": "text/event-stream" },
- }),
- )
- const resolved = yield* Provider.use.getModel(
- ProviderV2.ID.make(vivgridFixture.providerID),
- ModelV2.ID.make(fixture.model.id),
- )
- const sessionID = SessionID.make("session-test-1")
- const agent = {
- name: "test",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- temperature: 0.4,
- topP: 0.8,
- } satisfies Agent.Info
- const user = {
- id: MessageID.make("msg_user-1"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make(vivgridFixture.providerID), modelID: resolved.id, variant: "high" },
- } satisfies SessionV1.User
- yield* drain({
- user,
- sessionID,
- model: resolved,
- agent,
- system: ["You are a helpful assistant."],
- messages: [{ role: "user", content: "Hello" }],
- tools: {},
- })
- const capture = yield* Effect.promise(() => request)
- const body = capture.body
- const headers = capture.headers
- const url = capture.url
- expect(url.pathname.startsWith("/v1/")).toBe(true)
- expect(url.pathname.endsWith("/chat/completions")).toBe(true)
- expect(headers.get("Authorization")).toBe("Bearer test-key")
- expect(body.model).toBe(resolved.api.id)
- expect(body.temperature).toBe(0.4)
- expect(body.top_p).toBe(0.8)
- expect(body.stream).toBe(true)
- const maxTokens = (body.max_tokens as number | undefined) ?? (body.max_output_tokens as number | undefined)
- const expectedMaxTokens = ProviderTransform.maxOutputTokens(resolved)
- expect(maxTokens).toBe(expectedMaxTokens)
- const reasoning = (body.reasoningEffort as string | undefined) ?? (body.reasoning_effort as string | undefined)
- expect(reasoning).toBe("high")
- }),
- {
- config: () => ({
- enabled_providers: [vivgridFixture.providerID],
- provider: {
- [vivgridFixture.providerID]: {
- options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` },
- },
- },
- }),
- },
- )
- const cerebrasFixture = { providerID: "cerebras", modelID: "gpt-oss-120b" }
- it.instance(
- "replays Cerebras assistant reasoning using the provider-supported field",
- () =>
- Effect.gen(function* () {
- const fixture = loadFixture(cerebrasFixture.providerID, cerebrasFixture.modelID)
- const request = waitRequest(
- "/chat/completions",
- new Response(createChatStream("Hello"), {
- status: 200,
- headers: { "Content-Type": "text/event-stream" },
- }),
- )
- const resolved = yield* Provider.use.getModel(
- ProviderV2.ID.make(cerebrasFixture.providerID),
- ModelV2.ID.make(fixture.model.id),
- )
- const sessionID = SessionID.make("session-test-cerebras-reasoning")
- const agent = {
- name: "test",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- } satisfies Agent.Info
- const user = {
- id: MessageID.make("msg_user-cerebras-reasoning"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make(cerebrasFixture.providerID), modelID: resolved.id },
- } satisfies SessionV1.User
- yield* drain({
- user,
- sessionID,
- model: resolved,
- agent,
- system: ["You are a helpful assistant."],
- messages: [
- { role: "user", content: "Hello" },
- {
- role: "assistant",
- content: [
- { type: "reasoning", text: "thinking" },
- { type: "text", text: "Previous answer" },
- ],
- },
- { role: "user", content: "Continue" },
- ] satisfies ModelMessage[],
- tools: {},
- })
- const capture = yield* Effect.promise(() => request)
- const messages = capture.body.messages as Array<Record<string, unknown>>
- const assistant = messages.find((msg) => msg.role === "assistant")
- expect(assistant?.reasoning).toBe("thinking")
- expect(assistant && "reasoning_content" in assistant).toBe(false)
- }),
- {
- config: () => ({
- enabled_providers: [cerebrasFixture.providerID],
- provider: {
- [cerebrasFixture.providerID]: {
- options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` },
- },
- },
- }),
- },
- )
- const alibabaQwenFixture = { providerID: "alibaba", modelID: "qwen-plus" }
- it.instance(
- "service stream cancellation cancels provider response body promptly",
- () =>
- Effect.gen(function* () {
- const fixture = loadFixture(alibabaQwenFixture.providerID, alibabaQwenFixture.modelID)
- const pending = waitStreamingRequest("/chat/completions")
- const resolved = yield* Provider.use.getModel(
- ProviderV2.ID.make(alibabaQwenFixture.providerID),
- ModelV2.ID.make(fixture.model.id),
- )
- const sessionID = SessionID.make("session-test-service-abort")
- const agent = {
- name: "test",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- } satisfies Agent.Info
- const user = {
- id: MessageID.make("msg_user-service-abort"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
- } satisfies SessionV1.User
- const fiber = yield* drain({
- user,
- sessionID,
- model: resolved,
- agent,
- system: ["You are a helpful assistant."],
- messages: [{ role: "user", content: "Hello" }],
- tools: {},
- }).pipe(Effect.exit, Effect.forkScoped)
- yield* Effect.promise(() => pending.request)
- yield* Fiber.interrupt(fiber)
- yield* Effect.promise(() => Promise.race([pending.responseCanceled, timeout(500)]))
- const exit = yield* Fiber.await(fiber)
- // Fiber.await returns an Exit<Exit<...>>. Unwrap once.
- const inner = Exit.isSuccess(exit) ? exit.value : exit
- expect(Exit.isFailure(inner)).toBe(true)
- if (Exit.isFailure(inner)) {
- expect(Cause.hasInterrupts(inner.cause)).toBe(true)
- }
- yield* Effect.promise(() => Promise.race([pending.requestAborted, timeout(500)]).catch(() => undefined))
- }),
- {
- config: () => ({
- enabled_providers: [alibabaQwenFixture.providerID],
- provider: {
- [alibabaQwenFixture.providerID]: {
- options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` },
- },
- },
- }),
- },
- )
- it.instance(
- "keeps tools enabled by prompt permissions",
- () =>
- Effect.gen(function* () {
- const fixture = loadFixture(alibabaQwenFixture.providerID, alibabaQwenFixture.modelID)
- const request = waitRequest(
- "/chat/completions",
- new Response(createChatStream("Hello"), {
- status: 200,
- headers: { "Content-Type": "text/event-stream" },
- }),
- )
- const resolved = yield* Provider.use.getModel(
- ProviderV2.ID.make(alibabaQwenFixture.providerID),
- ModelV2.ID.make(fixture.model.id),
- )
- const sessionID = SessionID.make("session-test-tools")
- const agent = {
- name: "test",
- mode: "primary",
- options: {},
- permission: [{ permission: "question", pattern: "*", action: "deny" }],
- } satisfies Agent.Info
- const user = {
- id: MessageID.make("msg_user-tools"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
- tools: { question: true },
- } satisfies SessionV1.User
- yield* drain({
- user,
- sessionID,
- model: resolved,
- agent,
- permission: [{ permission: "question", pattern: "*", action: "allow" }],
- system: ["You are a helpful assistant."],
- messages: [{ role: "user", content: "Hello" }],
- tools: {
- question: tool({
- description: "Ask a question",
- inputSchema: z.object({}),
- execute: async () => ({ output: "" }),
- }),
- },
- })
- const capture = yield* Effect.promise(() => request)
- const tools = capture.body.tools as Array<{ function?: { name?: string } }> | undefined
- expect(tools?.some((item) => item.function?.name === "question")).toBe(true)
- }),
- {
- config: () => ({
- enabled_providers: [alibabaQwenFixture.providerID],
- provider: {
- [alibabaQwenFixture.providerID]: {
- options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` },
- },
- },
- }),
- },
- )
- it.instance(
- "sends responses API payload for OpenAI models",
- () =>
- Effect.gen(function* () {
- const model = loadFixture("openai", "gpt-5.2").model
- const responseChunks = [
- {
- type: "response.created",
- response: {
- id: "resp-1",
- created_at: Math.floor(Date.now() / 1000),
- model: model.id,
- service_tier: null,
- },
- },
- {
- type: "response.output_item.added",
- output_index: 0,
- item: { type: "message", id: "item-1", status: "in_progress", role: "assistant", content: [] },
- },
- {
- type: "response.content_part.added",
- item_id: "item-1",
- output_index: 0,
- content_index: 0,
- part: { type: "output_text", text: "", annotations: [] },
- },
- {
- type: "response.output_text.delta",
- item_id: "item-1",
- delta: "Hello",
- logprobs: null,
- },
- {
- type: "response.completed",
- response: {
- incomplete_details: null,
- usage: {
- input_tokens: 1,
- input_tokens_details: null,
- output_tokens: 1,
- output_tokens_details: null,
- },
- service_tier: null,
- },
- },
- ]
- const request = waitRequest("/responses", createEventResponse(responseChunks, true))
- 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",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- temperature: 0.2,
- } satisfies Agent.Info
- const user = {
- id: MessageID.make("msg_user-2"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" },
- } satisfies SessionV1.User
- yield* drain({
- user,
- sessionID,
- model: resolved,
- agent,
- system: ["You are a helpful assistant."],
- messages: [{ role: "user", content: "Hello" }],
- tools: {},
- })
- const capture = yield* Effect.promise(() => request)
- const body = capture.body
- expect(capture.url.pathname.endsWith("/responses")).toBe(true)
- expect(body.model).toBe(resolved.api.id)
- expect(body.stream).toBe(true)
- expect((body.reasoning as { effort?: string } | undefined)?.effort).toBe("high")
- const maxTokens = body.max_output_tokens as number | undefined
- expect(maxTokens).toBe(undefined) // match codex cli behavior
- }),
- { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, `${state.server!.url.origin}/v1`) },
- )
- it.instance(
- "keeps supported OpenAI models on AI SDK path when native flag is off",
- () =>
- Effect.gen(function* () {
- const model = loadFixture("openai", "gpt-5.2").model
- const request = waitRequest(
- "/responses",
- createEventResponse(
- [
- {
- type: "response.created",
- response: {
- id: "resp-flag-off",
- created_at: Math.floor(Date.now() / 1000),
- model: model.id,
- service_tier: null,
- },
- },
- {
- type: "response.output_item.added",
- output_index: 0,
- item: { type: "message", id: "item-flag-off", status: "in_progress", role: "assistant", content: [] },
- },
- {
- type: "response.content_part.added",
- item_id: "item-flag-off",
- output_index: 0,
- content_index: 0,
- part: { type: "output_text", text: "", annotations: [] },
- },
- {
- type: "response.output_text.delta",
- item_id: "item-flag-off",
- delta: "Flag off",
- logprobs: null,
- },
- {
- type: "response.completed",
- response: {
- incomplete_details: null,
- usage: {
- input_tokens: 1,
- input_tokens_details: null,
- output_tokens: 1,
- output_tokens_details: null,
- },
- service_tier: null,
- },
- },
- ],
- true,
- ),
- )
- const failingNativeClient = Layer.succeed(
- LLMClient.Service,
- LLMClient.Service.of({
- prepare: () => Effect.die(new Error("native LLM client should not be used when the flag is off")),
- stream: () => Stream.die(new Error("native LLM client should not be used when the flag is off")),
- generate: () => Effect.die(new Error("native LLM client should not be used when the flag is off")),
- }),
- )
- 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",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- } satisfies Agent.Info
- yield* drainWith(
- AppNodeBuilder.build(LLM.node, [
- [LayerNodePlatform.llmClient, failingNativeClient],
- [RuntimeFlags.node, RuntimeFlags.layer({ experimentalNativeLlm: false })],
- ]),
- {
- user: {
- id: MessageID.make("msg_user-native-flag-off"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" },
- } satisfies SessionV1.User,
- sessionID,
- model: resolved,
- agent,
- system: ["You are a helpful assistant."],
- messages: [{ role: "user", content: "Hello" }],
- tools: {},
- },
- )
- const capture = yield* Effect.promise(() => request)
- expect(capture.url.pathname.endsWith("/responses")).toBe(true)
- expect(capture.body.model).toBe(resolved.api.id)
- }),
- { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, `${state.server!.url.origin}/v1`) },
- )
- it.instance(
- "streams OpenAI through native runtime when opted in",
- () =>
- Effect.gen(function* () {
- const model = loadFixture("openai", "gpt-5.2").model
- const chunks = [
- { type: "response.created", response: { id: "resp-native" } },
- {
- type: "response.output_item.added",
- item: { type: "message", id: "item-native", status: "in_progress" },
- },
- { type: "response.output_text.delta", item_id: "item-native", delta: "Hello native" },
- {
- type: "response.completed",
- response: {
- incomplete_details: null,
- usage: {
- input_tokens: 1,
- input_tokens_details: null,
- output_tokens: 1,
- output_tokens_details: null,
- },
- },
- },
- ]
- const request = waitRequest("/responses", createEventResponse(chunks, true))
- 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",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- temperature: 0.2,
- } satisfies Agent.Info
- yield* drainWith(llmLayerWithExecutor({ flags: { experimentalNativeLlm: true } }), {
- user: {
- id: MessageID.make("msg_user-native"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" },
- } satisfies SessionV1.User,
- sessionID,
- model: resolved,
- agent,
- system: ["You are a helpful assistant."],
- messages: [{ role: "user", content: "Hello" }],
- tools: {},
- })
- const capture = yield* Effect.promise(() => request)
- expect(capture.url.pathname.endsWith("/responses")).toBe(true)
- expect(capture.headers.get("Authorization")).toBe("Bearer test-openai-key")
- expect(capture.body.model).toBe(model.id)
- expect(capture.body.stream).toBe(true)
- expect((capture.body.reasoning as { effort?: string } | undefined)?.effort).toBe("high")
- expect(capture.body.include).toEqual(["reasoning.encrypted_content"])
- expect(JSON.stringify(capture.body.input)).toContain("You are a helpful assistant.")
- expect(capture.body.input).toContainEqual({ role: "user", content: [{ type: "input_text", text: "Hello" }] })
- }),
- { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, `${state.server!.url.origin}/v1`) },
- )
- it.instance(
- "uses injected native request executor for tool calls",
- () =>
- Effect.gen(function* () {
- const model = loadFixture("openai", "gpt-5.2").model
- const chunks = [
- {
- type: "response.output_item.added",
- item: { type: "function_call", id: "item-injected-tool", call_id: "call-injected-tool", name: "lookup" },
- },
- {
- type: "response.function_call_arguments.delta",
- item_id: "item-injected-tool",
- delta: '{"query":"weather"}',
- },
- {
- type: "response.output_item.done",
- item: {
- type: "function_call",
- id: "item-injected-tool",
- call_id: "call-injected-tool",
- name: "lookup",
- arguments: '{"query":"weather"}',
- },
- },
- {
- type: "response.completed",
- response: { incomplete_details: null, usage: { input_tokens: 1, output_tokens: 1 } },
- },
- ]
- let captured: Record<string, unknown> | undefined
- let executed: unknown
- const executor = Layer.succeed(
- RequestExecutor.Service,
- RequestExecutor.Service.of({
- execute: (request) =>
- Effect.gen(function* () {
- const web = yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie)
- captured = (yield* Effect.promise(() => web.json())) as Record<string, unknown>
- return HttpClientResponse.fromWeb(request, createEventResponse(chunks, true))
- }),
- }),
- )
- 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",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- } satisfies Agent.Info
- yield* drainWith(llmLayerWithExecutor({ executor, flags: { experimentalNativeLlm: true } }), {
- user: {
- id: MessageID.make("msg_user-native-injected-tool"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id },
- } satisfies SessionV1.User,
- sessionID,
- model: resolved,
- agent,
- system: [],
- messages: [{ role: "user", content: "Use lookup" }],
- tools: {
- lookup: tool({
- description: "Lookup data",
- inputSchema: z.object({ query: z.string() }),
- execute: async (args, options) => {
- executed = { args, toolCallId: options.toolCallId }
- return { output: "looked up" }
- },
- }),
- },
- })
- expect(captured?.model).toBe(model.id)
- expect(captured?.tools).toEqual([
- {
- type: "function",
- name: "lookup",
- description: "Lookup data",
- strict: false,
- parameters: {
- type: "object",
- properties: { query: { type: "string" } },
- required: ["query"],
- additionalProperties: false,
- $schema: "http://json-schema.org/draft-07/schema#",
- },
- },
- ])
- expect(executed).toEqual({ args: { query: "weather" }, toolCallId: "call-injected-tool" })
- }),
- { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, "https://injected-openai.test/v1") },
- )
- it.instance(
- "executes OpenAI tool calls through native runtime",
- () =>
- Effect.gen(function* () {
- const model = loadFixture("openai", "gpt-5.2").model
- const chunks = [
- {
- type: "response.output_item.added",
- item: { type: "function_call", id: "item-native-tool", call_id: "call-native-tool", name: "lookup" },
- },
- {
- type: "response.function_call_arguments.delta",
- item_id: "item-native-tool",
- delta: '{"query":"weather"}',
- },
- {
- type: "response.output_item.done",
- item: {
- type: "function_call",
- id: "item-native-tool",
- call_id: "call-native-tool",
- name: "lookup",
- arguments: '{"query":"weather"}',
- },
- },
- {
- type: "response.completed",
- response: { incomplete_details: null, usage: { input_tokens: 1, output_tokens: 1 } },
- },
- ]
- const request = waitRequest("/responses", createEventResponse(chunks, true))
- let executed: unknown
- 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",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- } satisfies Agent.Info
- yield* drainWith(llmLayerWithExecutor({ flags: { experimentalNativeLlm: true } }), {
- user: {
- id: MessageID.make("msg_user-native-tool"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id },
- } satisfies SessionV1.User,
- sessionID,
- model: resolved,
- agent,
- system: [],
- messages: [{ role: "user", content: "Use lookup" }],
- tools: {
- lookup: tool({
- description: "Lookup data",
- inputSchema: z.object({ query: z.string() }),
- execute: async (args, options) => {
- executed = { args, toolCallId: options.toolCallId }
- return { output: "looked up" }
- },
- }),
- },
- })
- const capture = yield* Effect.promise(() => request)
- expect(capture.body.tools).toEqual([
- {
- type: "function",
- name: "lookup",
- description: "Lookup data",
- strict: false,
- parameters: {
- type: "object",
- properties: { query: { type: "string" } },
- required: ["query"],
- additionalProperties: false,
- $schema: "http://json-schema.org/draft-07/schema#",
- },
- },
- ])
- expect(executed).toEqual({ args: { query: "weather" }, toolCallId: "call-native-tool" })
- }),
- {
- config: () => {
- const model = loadFixture("openai", "gpt-5.2").model
- return {
- enabled_providers: ["openai"],
- provider: {
- openai: {
- name: "OpenAI",
- env: ["OPENAI_API_KEY"],
- npm: "@ai-sdk/openai",
- api: "https://api.openai.com/v1",
- models: { [model.id]: JSON.parse(JSON.stringify(model)) as ConfigModel },
- options: { apiKey: "test-openai-key", baseURL: `${state.server!.url.origin}/v1` },
- },
- },
- }
- },
- },
- )
- it.instance(
- "accepts user image attachments as data URLs for OpenAI models",
- () =>
- Effect.gen(function* () {
- const model = loadFixture("openai", "gpt-5.2").model
- const chunks = [
- {
- type: "response.created",
- response: {
- id: "resp-data-url",
- created_at: Math.floor(Date.now() / 1000),
- model: model.id,
- service_tier: null,
- },
- },
- {
- type: "response.output_item.added",
- output_index: 0,
- item: { type: "message", id: "item-data-url", status: "in_progress", role: "assistant", content: [] },
- },
- {
- type: "response.content_part.added",
- item_id: "item-data-url",
- output_index: 0,
- content_index: 0,
- part: { type: "output_text", text: "", annotations: [] },
- },
- {
- type: "response.output_text.delta",
- item_id: "item-data-url",
- delta: "Looks good",
- logprobs: null,
- },
- {
- type: "response.completed",
- response: {
- incomplete_details: null,
- usage: {
- input_tokens: 1,
- input_tokens_details: null,
- output_tokens: 1,
- output_tokens_details: null,
- },
- service_tier: null,
- },
- },
- ]
- const request = waitRequest("/responses", createEventResponse(chunks, true))
- const image = `data:image/png;base64,${Buffer.from(
- yield* Effect.promise(() =>
- Bun.file(path.join(import.meta.dir, "../tool/fixtures/large-image.png")).arrayBuffer(),
- ),
- ).toString("base64")}`
- 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",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- } satisfies Agent.Info
- const user = {
- id: MessageID.make("msg_user-data-url"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id },
- } satisfies SessionV1.User
- yield* drain({
- user,
- sessionID,
- model: resolved,
- agent,
- system: ["You are a helpful assistant."],
- messages: [
- {
- role: "user",
- content: [
- { type: "text", text: "Describe this image" },
- { type: "file", mediaType: "image/png", filename: "large-image.png", data: image },
- ],
- },
- ] as ModelMessage[],
- tools: {},
- })
- const capture = yield* Effect.promise(() => request)
- expect(capture.url.pathname.endsWith("/responses")).toBe(true)
- }),
- { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, `${state.server!.url.origin}/v1`) },
- )
- const minimaxFixture = { providerID: "minimax", modelID: "MiniMax-M2.5" }
- it.instance(
- "sends messages API payload for Anthropic Compatible models",
- () =>
- Effect.gen(function* () {
- const model = loadFixture(minimaxFixture.providerID, minimaxFixture.modelID).model
- const chunks = [
- {
- type: "message_start",
- message: {
- id: "msg-1",
- model: model.id,
- usage: {
- input_tokens: 3,
- cache_creation_input_tokens: null,
- cache_read_input_tokens: null,
- },
- },
- },
- {
- type: "content_block_start",
- index: 0,
- content_block: { type: "text", text: "" },
- },
- {
- type: "content_block_delta",
- index: 0,
- delta: { type: "text_delta", text: "Hello" },
- },
- { type: "content_block_stop", index: 0 },
- {
- type: "message_delta",
- delta: { stop_reason: "end_turn", stop_sequence: null, container: null },
- usage: {
- input_tokens: 3,
- output_tokens: 2,
- cache_creation_input_tokens: null,
- cache_read_input_tokens: null,
- },
- },
- { type: "message_stop" },
- ]
- const request = waitRequest("/messages", createEventResponse(chunks))
- const resolved = yield* Provider.use.getModel(
- ProviderV2.ID.make(minimaxFixture.providerID),
- ModelV2.ID.make(model.id),
- )
- const sessionID = SessionID.make("session-test-3")
- const agent = {
- name: "test",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- temperature: 0.4,
- topP: 0.9,
- } satisfies Agent.Info
- const user = {
- id: MessageID.make("msg_user-3"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make("minimax"), modelID: ModelV2.ID.make("MiniMax-M2.5") },
- } satisfies SessionV1.User
- yield* drain({
- user,
- sessionID,
- model: resolved,
- agent,
- system: ["You are a helpful assistant."],
- messages: [{ role: "user", content: "Hello" }],
- tools: {},
- })
- const capture = yield* Effect.promise(() => request)
- const body = capture.body
- expect(capture.url.pathname.endsWith("/messages")).toBe(true)
- expect(body.model).toBe(resolved.api.id)
- expect(body.max_tokens).toBe(ProviderTransform.maxOutputTokens(resolved))
- expect(body.temperature).toBe(0.4)
- expect(body.top_p).toBe(0.9)
- }),
- {
- config: () => ({
- enabled_providers: [minimaxFixture.providerID],
- provider: {
- [minimaxFixture.providerID]: {
- options: { apiKey: "test-anthropic-key", baseURL: `${state.server!.url.origin}/v1` },
- },
- },
- }),
- },
- )
- it.instance(
- "sends anthropic tool_use blocks with tool_result immediately after them",
- () =>
- Effect.gen(function* () {
- const model = loadFixture("anthropic", "claude-opus-4-6").model
- const chunks = [
- {
- type: "message_start",
- message: {
- id: "msg-tool-order",
- model: model.id,
- usage: {
- input_tokens: 3,
- cache_creation_input_tokens: null,
- cache_read_input_tokens: null,
- },
- },
- },
- {
- type: "content_block_start",
- index: 0,
- content_block: { type: "text", text: "" },
- },
- {
- type: "content_block_delta",
- index: 0,
- delta: { type: "text_delta", text: "ok" },
- },
- { type: "content_block_stop", index: 0 },
- {
- type: "message_delta",
- delta: { stop_reason: "end_turn", stop_sequence: null, container: null },
- usage: {
- input_tokens: 3,
- output_tokens: 2,
- cache_creation_input_tokens: null,
- cache_read_input_tokens: null,
- },
- },
- { type: "message_stop" },
- ]
- const request = waitRequest("/messages", createEventResponse(chunks))
- const resolved = yield* Provider.use.getModel(ProviderV2.ID.make("anthropic"), ModelV2.ID.make(model.id))
- const sessionID = SessionID.make("session-test-anthropic-tools")
- const agent = {
- name: "test",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- } satisfies Agent.Info
- const user = {
- id: MessageID.make("msg_user-anthropic-tools"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make("anthropic"), modelID: resolved.id, variant: "max" },
- } satisfies SessionV1.User
- const input = [
- {
- info: {
- id: "msg_user",
- sessionID,
- role: "user",
- time: { created: 1 },
- agent: "gentleman",
- model: { providerID: "anthropic", modelID: "claude-opus-4-6", variant: "max" },
- },
- parts: [
- {
- id: "p_user",
- sessionID,
- messageID: "msg_user",
- type: "text",
- text: "Can you check whether there are any PDF files in my home directory?",
- },
- ],
- },
- {
- info: {
- id: "msg_call",
- sessionID,
- parentID: "msg_user",
- role: "assistant",
- mode: "gentleman",
- agent: "gentleman",
- variant: "max",
- path: { cwd: "/root", root: "/" },
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- modelID: "claude-opus-4-6",
- providerID: "anthropic",
- time: { created: 2, completed: 3 },
- finish: "tool-calls",
- },
- parts: [
- {
- id: "p_step",
- sessionID,
- messageID: "msg_call",
- type: "step-start",
- },
- {
- id: "p_read",
- sessionID,
- messageID: "msg_call",
- type: "tool",
- tool: "read",
- callID: "toolu_01N8mDEzG8DSTs7UPHFtmgCT",
- state: {
- status: "completed",
- input: { filePath: "/root" },
- output: "<path>/root</path>",
- metadata: {},
- title: "root",
- time: { start: 10, end: 11 },
- },
- },
- {
- id: "p_glob",
- sessionID,
- messageID: "msg_call",
- type: "tool",
- tool: "glob",
- callID: "toolu_01APxrADs7VozN8uWzw9WwHr",
- state: {
- status: "completed",
- input: { pattern: "**/*.pdf", path: "/root" },
- output: "No files found",
- metadata: {},
- title: "root",
- time: { start: 12, end: 13 },
- },
- },
- {
- id: "p_text",
- sessionID,
- messageID: "msg_call",
- type: "text",
- text: "I checked your home directory and looked for PDF files.",
- time: { start: 14, end: 15 },
- },
- ],
- },
- ] as any[]
- const modelMessages = yield* Effect.promise(() => MessageV2.toModelMessages(input as any, resolved))
- yield* drain({
- user,
- sessionID,
- model: resolved,
- agent,
- system: [],
- messages: modelMessages,
- tools: {
- read: tool({
- description: "Stub read tool",
- inputSchema: z.object({ filePath: z.string() }),
- execute: async () => ({ output: "stub" }),
- }),
- glob: tool({
- description: "Stub glob tool",
- inputSchema: z.object({ pattern: z.string(), path: z.string().optional() }),
- execute: async () => ({ output: "stub" }),
- }),
- },
- })
- const capture = yield* Effect.promise(() => request)
- const body = capture.body
- expect(capture.url.pathname.endsWith("/messages")).toBe(true)
- const messages = body.messages as Array<{ role: string; content: Array<Record<string, unknown>> }>
- expect(messages[0]?.role).toBe("user")
- expect(messages[0]?.content[0]).toMatchObject({
- type: "text",
- text: "Can you check whether there are any PDF files in my home directory?",
- })
- expect(messages.some((message) => message.content.some((part) => "cache_control" in part))).toBe(true)
- const toolUseIndex = messages.findIndex((message) => message.content.some((part) => part.type === "tool_use"))
- expect(toolUseIndex).toBeGreaterThan(0)
- expect(messages[toolUseIndex].role).toBe("assistant")
- expect(messages[toolUseIndex].content.filter((part) => part.type === "tool_use")).toMatchObject([
- {
- type: "tool_use",
- id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT",
- name: "read",
- input: { filePath: "/root" },
- },
- {
- type: "tool_use",
- id: "toolu_01APxrADs7VozN8uWzw9WwHr",
- name: "glob",
- input: { pattern: "**/*.pdf", path: "/root" },
- },
- ])
- expect(messages[toolUseIndex + 1]).toMatchObject({
- role: "user",
- content: [
- { type: "tool_result", tool_use_id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT", content: "<path>/root</path>" },
- { type: "tool_result", tool_use_id: "toolu_01APxrADs7VozN8uWzw9WwHr", content: "No files found" },
- ],
- })
- }),
- {
- config: () => {
- const model = loadFixture("anthropic", "claude-opus-4-6").model
- return {
- enabled_providers: ["anthropic"],
- provider: {
- anthropic: {
- name: "Anthropic",
- env: ["ANTHROPIC_API_KEY"],
- npm: "@ai-sdk/anthropic",
- api: "https://api.anthropic.com/v1",
- models: { [model.id]: configModel(model) as ConfigModel },
- options: { apiKey: "test-anthropic-key", baseURL: `${state.server!.url.origin}/v1` },
- },
- },
- }
- },
- },
- )
- const geminiFixture = { providerID: "google", modelID: "gemini-2.5-flash" }
- it.instance(
- "sends Google API payload for Gemini models",
- () =>
- Effect.gen(function* () {
- const model = loadFixture(geminiFixture.providerID, geminiFixture.modelID).model
- const pathSuffix = `/v1beta/models/${model.id}:streamGenerateContent`
- const chunks = [
- {
- candidates: [{ content: { parts: [{ text: "Hello" }] }, finishReason: "STOP" }],
- usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 },
- },
- ]
- const request = waitRequest(pathSuffix, createEventResponse(chunks))
- const resolved = yield* Provider.use.getModel(
- ProviderV2.ID.make(geminiFixture.providerID),
- ModelV2.ID.make(model.id),
- )
- const sessionID = SessionID.make("session-test-4")
- const agent = {
- name: "test",
- mode: "primary",
- options: {},
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- temperature: 0.3,
- topP: 0.8,
- } satisfies Agent.Info
- const user = {
- id: MessageID.make("msg_user-4"),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: agent.name,
- model: { providerID: ProviderV2.ID.make(geminiFixture.providerID), modelID: resolved.id },
- } satisfies SessionV1.User
- yield* drain({
- user,
- sessionID,
- model: resolved,
- agent,
- system: ["You are a helpful assistant."],
- messages: [
- { role: "user", content: "Hello" },
- { role: "assistant", content: [{ type: "reasoning", text: "" }] },
- ],
- tools: {},
- })
- const capture = yield* Effect.promise(() => request)
- const body = capture.body
- const config = body.generationConfig as
- | { temperature?: number; topP?: number; maxOutputTokens?: number }
- | undefined
- expect(capture.url.pathname).toBe(pathSuffix)
- expect(body.contents).toEqual([{ role: "user", parts: [{ text: "Hello" }] }])
- expect(config?.temperature).toBe(0.3)
- expect(config?.topP).toBe(0.8)
- expect(config?.maxOutputTokens).toBe(ProviderTransform.maxOutputTokens(resolved))
- }),
- {
- config: () => ({
- enabled_providers: [geminiFixture.providerID],
- provider: {
- [geminiFixture.providerID]: {
- options: { apiKey: "test-google-key", baseURL: `${state.server!.url.origin}/v1beta` },
- },
- },
- }),
- },
- )
- })
|