/** * Custom streamSimple for OpenCode Zen/Go that delegates to the correct pi-ai * built-in streamer based on the resolved per-model protocol. * * This is where the routing fix actually takes effect: instead of relying on * the (sometimes wrong) model.api registered by the built-in providers, we * re-derive the protocol from the catalog, override the model's api + baseUrl, * inject the gotcha onPayload hooks, and hand off to the matching streamer. */ // NOTE: import the streamers from the MAIN entry, not the "/anthropic" etc. // subpaths. pi loads extensions via tsx, which resolves the main entry fine // (cf. pi-commandcode-provider) but mis-resolves ESM-only subpath exports // ("Cannot find module .../dist/index.js/anthropic"). The main entry // re-exports all four streamSimple* functions. import { streamSimpleAnthropic, streamSimpleOpenAICompletions, streamSimpleOpenAIResponses, streamSimpleGoogle, } from "@earendil-works/pi-ai" import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions, } from "@earendil-works/pi-ai" import { composeOnPayload } from "./gotchas.ts" import { resolveOpencodeApiKey, isEnvLiteral } from "./auth.ts" import { getBaseUrlForProtocol, getProtocolForModel } from "./catalog.ts" import type { OpencodeTier } from "../../types.ts" /** * Return a usable API key: prefer the one pi-ai already resolved and passed in * options; fall back to direct env/auth-file resolution when it is missing or * still carries the "$OPENCODE_API_KEY" literal placeholder. */ function resolveApiKey(provided: string | undefined): string | undefined { if (provided && !isEnvLiteral(provided)) return provided return resolveOpencodeApiKey() } /** Streamer functions delegated to (injectable for tests). */ export interface OpencodeStreamers { anthropic: typeof streamSimpleAnthropic openaiResponses: typeof streamSimpleOpenAIResponses openaiCompletions: typeof streamSimpleOpenAICompletions google: typeof streamSimpleGoogle } const DEFAULT_STREAMERS: OpencodeStreamers = { anthropic: streamSimpleAnthropic, openaiResponses: streamSimpleOpenAIResponses, openaiCompletions: streamSimpleOpenAICompletions, google: streamSimpleGoogle, } /** * Create a streamSimple function bound to a tier ("zen" | "go"). * * The returned function matches pi-ai's StreamFunction signature so it can be * passed directly to pi.registerProvider({ streamSimple }). * * `streamers` is injectable so tests can assert delegation + gotcha wiring * without hitting the real pi-ai streamers; production omits it and uses the * real re-exported streamers. */ export function createOpencodeStreamSimple( tier: OpencodeTier, streamers: OpencodeStreamers = DEFAULT_STREAMERS, ) { return function opencodeStreamSimple( model: Model, context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream { const protocol = getProtocolForModel(tier, model.id) const baseUrl = getBaseUrlForProtocol(tier, protocol) // Re-pin api + baseUrl so delegation targets the CORRECT endpoint even // when a live/passthrough model arrived with a stale api. const resolvedModel = { ...model, api: protocol, baseUrl } as Model // Chain the caller's onPayload (user transforms first) with the gotcha // fixes (run last, closest to the wire). const callerOnPayload = options?.onPayload const gotchaTransform = composeOnPayload(protocol, model.id) const composedOnPayload = async ( payload: unknown, m: Model, ): Promise => { let result = payload if (callerOnPayload) { const transformed = await callerOnPayload(result, m) if (transformed !== undefined) result = transformed } return gotchaTransform(result) } const resolvedOptions: SimpleStreamOptions = { ...options, apiKey: resolveApiKey(options?.apiKey), onPayload: composedOnPayload, } switch (protocol) { case "anthropic-messages": return streamers.anthropic( resolvedModel as Model<"anthropic-messages">, context, resolvedOptions, ) case "openai-responses": return streamers.openaiResponses( resolvedModel as Model<"openai-responses">, context, resolvedOptions, ) case "google-generative-ai": return streamers.google( resolvedModel as Model<"google-generative-ai">, context, resolvedOptions, ) case "openai-completions": return streamers.openaiCompletions( resolvedModel as Model<"openai-completions">, context, resolvedOptions, ) } } }