summaryrefslogtreecommitdiffhomepage
path: root/packages/openai-stream/src/provider.ts
blob: 9a9369f29c619f5ad67b6dde6a62152728c01868 (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
74
75
76
77
78
79
80
81
82
import type {
  ApiKeyCredentials,
  ChatMessage,
  ModelInfo,
  ProviderContract,
  ProviderStreamOptions,
  ProviderUsage,
  ToolContract,
} from "@dispatch/kernel";
import type { FetchLike } from "@dispatch/trace-replay";
import { getUsage as fetchUsage } from "./getUsage.js";
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 } : {}),
      }),
    getUsage: (): Promise<ProviderUsage | undefined> =>
      fetchUsage({
        baseURL,
        apiKey,
        providerId: opts.id,
        ...(fetchFn !== undefined ? { fetchFn } : {}),
      }),
  };
}