summaryrefslogtreecommitdiffhomepage
path: root/packages/openai-stream/src/provider.ts
blob: c13d60ea58bc8ca2be5a8b64023c3f247e366242 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import type {
	ApiKeyCredentials,
	ChatMessage,
	ModelInfo,
	ProviderContract,
	ProviderStreamOptions,
	ToolContract,
} from "@dispatch/kernel";
import type { FetchLike } from "@dispatch/trace-replay";
import { listModels as fetchModels } from "./listModels.js";
import { streamChat } from "./stream.js";

/**
 * Generic factory for an OpenAI-compatible provider. A provider extension
 * supplies its own `id` (stamped on the ProviderContract + used in listModels
 * error labels) and an optional `transformBody` hook to add provider-specific
 * body fields (e.g. `reasoning_effort`) before the request is sent. The library
 * names no concrete feature — those knobs belong to the extension layer.
 */

export interface CreateOpenAICompatProviderOpts {
	readonly credentials: ApiKeyCredentials;
	readonly model: string;
	/** Provider id (was hardcoded "openai-compat"). Stamped on the ProviderContract.id
	 *  + used in listModels error labels. */
	readonly id: string;
	/**
	 * Internal injectable fetch — used by tests and replay mode.
	 * When absent, falls back to globalThis.fetch (production default).
	 */
	readonly fetchFn?: FetchLike;
	/**
	 * Optional hook a provider extension uses to add provider-specific body fields (e.g.
	 * `reasoning_effort`) before the request is sent. Receives the body built so far +
	 * the ProviderStreamOptions; returns ADDITIONAL fields to merge (or the full body).
	 * Default (absent): no extra fields. Generic — the library names no feature.
	 */
	readonly transformBody?: (
		body: Record<string, unknown>,
		opts: ProviderStreamOptions,
	) => Record<string, unknown>;
}

export function createOpenAICompatProvider(opts: CreateOpenAICompatProviderOpts): ProviderContract {
	const baseURL = opts.credentials.baseURL ?? "https://opencode.ai/zen/go/v1";
	const apiKey = opts.credentials.apiKey;
	const fetchFn = opts.fetchFn;
	const transformBody = opts.transformBody;

	const streamConfig = {
		baseURL,
		apiKey,
		model: opts.model,
		...(fetchFn !== undefined ? { fetchFn } : {}),
		...(transformBody !== undefined ? { transformBody } : {}),
	};

	return {
		id: opts.id,
		stream: (
			messages: readonly ChatMessage[],
			tools: readonly ToolContract[],
			streamOpts?: ProviderStreamOptions,
		) => streamChat(streamConfig, messages, tools, streamOpts),
		listModels: (): Promise<readonly ModelInfo[]> =>
			fetchModels({
				baseURL,
				apiKey,
				providerId: opts.id,
				...(fetchFn !== undefined ? { fetchFn } : {}),
			}),
	};
}