From 37b2c91937c92f928bdb7496ed974ea90a0b1746 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 10 Sep 2026 16:33:50 +0000 Subject: [PATCH] fix(core): attribute one-shot generation requests --- packages/core/src/generate.ts | 28 +++++++++----- packages/core/test/generate.test.ts | 57 ++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/packages/core/src/generate.ts b/packages/core/src/generate.ts index e3f558fd790d..d64d25ae302b 100644 --- a/packages/core/src/generate.ts +++ b/packages/core/src/generate.ts @@ -1,6 +1,7 @@ export * as Generate from "./generate.js" import { LLM, LLMClient, AIError } from "@opencode/ai" +import { SessionID } from "@opencode/schema/session-id" import { Context, Effect, Layer, Schema } from "effect" import { makeLocationNode } from "@opencode/util/effect/app-node" import { llmClient } from "./effect/app-node-platform.js" @@ -58,15 +59,24 @@ export const layer = Layer.effect( ? `Model unavailable: ${input.model.providerID}/${input.model.id}` : "No model specified and no supported model is available", }) - const response = yield* llm.generate(LLM.request({ model: resolved.model, prompt: input.prompt })).pipe( - Effect.mapError( - (error: AIError) => - new UnavailableError({ - message: error.message, - service: resolved.ref.providerID, - }), - ), - ) + const response = yield* llm + .generate( + LLM.request({ + model: resolved.model, + prompt: input.prompt, + // Gateways require session attribution even for a stateless call; no Session is stored. + http: { headers: { "x-opencode-session": SessionID.create() } }, + }), + ) + .pipe( + Effect.mapError( + (error: AIError) => + new UnavailableError({ + message: error.message, + service: resolved.ref.providerID, + }), + ), + ) return response.text }) diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts index 65624e217738..0bc623da4f6f 100644 --- a/packages/core/test/generate.test.ts +++ b/packages/core/test/generate.test.ts @@ -1,5 +1,6 @@ import { expect } from "bun:test" -import { LanguageModel } from "@opencode/ai" +import { LanguageModel, LLMClient } from "@opencode/ai" +import { RequestExecutor } from "@opencode/ai/route" import { OpenAIChat } from "@opencode/ai/protocols" import { TestLLM } from "@opencode/ai/testing" import { AISDK } from "@opencode/core/aisdk" @@ -11,6 +12,7 @@ import { ID, Info, Ref } from "@opencode/core/model" import { Provider } from "@opencode/core/provider" import { Npm } from "@opencode/util/npm" import { Effect, Layer } from "effect" +import { HttpClient, HttpClientResponse } from "effect/unstable/http" import { testEffect } from "./lib/effect" const selected = Info.make({ @@ -98,3 +100,56 @@ resolverIt.effect("resolves dynamic models with their catalog metadata", () => }) }), ) + +testEffect(Layer.empty).effect("attributes each stateless completion without creating a stored session", () => + Effect.gen(function* () { + const sessions: string[] = [] + const http = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.sync(() => { + const session = request.headers["x-opencode-session"] + if (!session) + return HttpClientResponse.fromWeb( + request, + Response.json( + { + error: { type: "MissingSessionID", message: "Session ID is required" }, + }, + { status: 400 }, + ), + ) + sessions.push(session) + return HttpClientResponse.fromWeb( + request, + new Response( + `data: ${JSON.stringify({ + id: "completion", + object: "chat.completion.chunk", + created: 1, + model: "gemini", + choices: [{ index: 0, delta: { content: "OK" }, finish_reason: "stop" }], + })}\n\ndata: [DONE]\n\n`, + { headers: { "content-type": "text/event-stream" } }, + ), + ) + }), + ), + ) + const native = LLMClient.layer.pipe(Layer.provide(RequestExecutor.layer.pipe(Layer.provide(http)))) + yield* Effect.gen(function* () { + const generate = yield* Generate.Service + for (let index = 0; index < 2; index++) { + expect( + yield* generate.text({ + prompt: "Return exactly OK", + model: Ref.make({ providerID: selected.providerID, id: selected.id }), + }), + ).toBe("OK") + } + }).pipe(Effect.provide(Generate.layer.pipe(Layer.provide(Layer.merge(resolver, native))))) + expect(sessions).toHaveLength(2) + expect(sessions[0]).toStartWith("ses_") + expect(sessions[1]).not.toBe(sessions[0]) + }), +)