offrouter-core 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/audit.d.ts +137 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +320 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/auth/credential-chain.d.ts +84 -0
  6. package/dist/auth/credential-chain.d.ts.map +1 -0
  7. package/dist/auth/credential-chain.js +150 -0
  8. package/dist/auth/credential-chain.js.map +1 -0
  9. package/dist/auth/index.d.ts +10 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +7 -0
  12. package/dist/auth/index.js.map +1 -0
  13. package/dist/auth/keychain.d.ts +60 -0
  14. package/dist/auth/keychain.d.ts.map +1 -0
  15. package/dist/auth/keychain.js +104 -0
  16. package/dist/auth/keychain.js.map +1 -0
  17. package/dist/auth/oauth-pkce.d.ts +138 -0
  18. package/dist/auth/oauth-pkce.d.ts.map +1 -0
  19. package/dist/auth/oauth-pkce.js +375 -0
  20. package/dist/auth/oauth-pkce.js.map +1 -0
  21. package/dist/auth/oauth-server.d.ts +36 -0
  22. package/dist/auth/oauth-server.d.ts.map +1 -0
  23. package/dist/auth/oauth-server.js +210 -0
  24. package/dist/auth/oauth-server.js.map +1 -0
  25. package/dist/config.d.ts +76 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +355 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/delegation.d.ts +123 -0
  30. package/dist/delegation.d.ts.map +1 -0
  31. package/dist/delegation.js +455 -0
  32. package/dist/delegation.js.map +1 -0
  33. package/dist/index.d.ts +41 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +26 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/policy.d.ts +45 -0
  38. package/dist/policy.d.ts.map +1 -0
  39. package/dist/policy.js +318 -0
  40. package/dist/policy.js.map +1 -0
  41. package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
  42. package/dist/protocol.d.ts.map +1 -0
  43. package/dist/protocol.js +6 -0
  44. package/dist/protocol.js.map +1 -0
  45. package/dist/providers/adapter.d.ts +137 -0
  46. package/dist/providers/adapter.d.ts.map +1 -0
  47. package/dist/providers/adapter.js +163 -0
  48. package/dist/providers/adapter.js.map +1 -0
  49. package/dist/providers/catalog-data.d.ts +128 -0
  50. package/dist/providers/catalog-data.d.ts.map +1 -0
  51. package/dist/providers/catalog-data.js +397 -0
  52. package/dist/providers/catalog-data.js.map +1 -0
  53. package/dist/providers/catalog.d.ts +63 -0
  54. package/dist/providers/catalog.d.ts.map +1 -0
  55. package/dist/providers/catalog.js +394 -0
  56. package/dist/providers/catalog.js.map +1 -0
  57. package/dist/providers/fake.d.ts +46 -0
  58. package/dist/providers/fake.d.ts.map +1 -0
  59. package/dist/providers/fake.js +234 -0
  60. package/dist/providers/fake.js.map +1 -0
  61. package/dist/providers/openai-compatible.d.ts +65 -0
  62. package/dist/providers/openai-compatible.d.ts.map +1 -0
  63. package/dist/providers/openai-compatible.js +434 -0
  64. package/dist/providers/openai-compatible.js.map +1 -0
  65. package/dist/proxy/responses-mapper.d.ts +366 -0
  66. package/dist/proxy/responses-mapper.d.ts.map +1 -0
  67. package/dist/proxy/responses-mapper.js +517 -0
  68. package/dist/proxy/responses-mapper.js.map +1 -0
  69. package/dist/proxy/responses-server.d.ts +29 -0
  70. package/dist/proxy/responses-server.d.ts.map +1 -0
  71. package/dist/proxy/responses-server.js +360 -0
  72. package/dist/proxy/responses-server.js.map +1 -0
  73. package/dist/router.d.ts +18 -0
  74. package/dist/router.d.ts.map +1 -0
  75. package/dist/router.js +296 -0
  76. package/dist/router.js.map +1 -0
  77. package/dist/schemas.d.ts +560 -0
  78. package/dist/schemas.d.ts.map +1 -0
  79. package/{src/schemas.ts → dist/schemas.js} +83 -103
  80. package/dist/schemas.js.map +1 -0
  81. package/dist/secrets.d.ts +112 -0
  82. package/dist/secrets.d.ts.map +1 -0
  83. package/dist/secrets.js +326 -0
  84. package/dist/secrets.js.map +1 -0
  85. package/dist/types.d.ts +117 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +6 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/usage-file.d.ts +59 -0
  90. package/dist/usage-file.d.ts.map +1 -0
  91. package/dist/usage-file.js +316 -0
  92. package/dist/usage-file.js.map +1 -0
  93. package/dist/usage.d.ts +235 -0
  94. package/dist/usage.d.ts.map +1 -0
  95. package/dist/usage.js +517 -0
  96. package/dist/usage.js.map +1 -0
  97. package/package.json +9 -4
  98. package/src/audit.test.ts +0 -302
  99. package/src/audit.ts +0 -553
  100. package/src/auth/credential-chain.test.ts +0 -326
  101. package/src/auth/credential-chain.ts +0 -235
  102. package/src/auth/index.ts +0 -45
  103. package/src/auth/keychain.test.ts +0 -265
  104. package/src/auth/keychain.ts +0 -168
  105. package/src/auth/oauth-pkce.test.ts +0 -329
  106. package/src/auth/oauth-pkce.ts +0 -571
  107. package/src/auth/oauth-server.test.ts +0 -83
  108. package/src/auth/oauth-server.ts +0 -296
  109. package/src/config.test.ts +0 -479
  110. package/src/config.ts +0 -505
  111. package/src/delegation.test.ts +0 -368
  112. package/src/delegation.ts +0 -605
  113. package/src/index.ts +0 -280
  114. package/src/policy.test.ts +0 -634
  115. package/src/policy.ts +0 -420
  116. package/src/providers/adapter.test.ts +0 -293
  117. package/src/providers/adapter.ts +0 -328
  118. package/src/providers/catalog-data.test.ts +0 -258
  119. package/src/providers/catalog-data.ts +0 -498
  120. package/src/providers/catalog.test.ts +0 -84
  121. package/src/providers/catalog.ts +0 -483
  122. package/src/providers/fake.ts +0 -312
  123. package/src/providers/openai-compatible.test.ts +0 -366
  124. package/src/providers/openai-compatible.ts +0 -554
  125. package/src/proxy/responses-mapper.test.ts +0 -290
  126. package/src/proxy/responses-mapper.ts +0 -736
  127. package/src/proxy/responses-server.test.ts +0 -322
  128. package/src/proxy/responses-server.ts +0 -469
  129. package/src/router.test.ts +0 -699
  130. package/src/router.ts +0 -352
  131. package/src/schemas.test.ts +0 -291
  132. package/src/secrets.test.ts +0 -271
  133. package/src/secrets.ts +0 -461
  134. package/src/types.ts +0 -173
  135. package/src/usage-file.test.ts +0 -243
  136. package/src/usage-file.ts +0 -435
  137. package/src/usage.test.ts +0 -335
  138. package/src/usage.ts +0 -859
  139. package/tsconfig.json +0 -9
@@ -1,554 +0,0 @@
1
- /**
2
- * OpenAI-compatible HTTP adapter (OpenRouter / xAI / Ollama shapes).
3
- * Tests inject fetch; live network only when caller enables it.
4
- *
5
- * Auth is presence-only: we set Authorization when a key is provided.
6
- * Ollama accepts a dummy key when none is configured.
7
- */
8
-
9
- import type { AuthTier, RouteRequest } from "../types.js";
10
- import {
11
- ProviderError,
12
- mapHttpToProviderError,
13
- type AuthStatus,
14
- type CostEstimate,
15
- type LimitSnapshot,
16
- type ModelCapability,
17
- type ProviderAdapter,
18
- type ProviderEvent,
19
- type ProviderInvokeRequest,
20
- } from "./adapter.js";
21
- import type { SecretStore } from "../secrets.js";
22
- import type { CredentialSource } from "../auth/credential-chain.js";
23
-
24
- export type OpenAiCompatibleFlavor = "openai" | "openrouter" | "xai" | "ollama";
25
-
26
- export interface OpenAiCompatibleOptions {
27
- id: string;
28
- baseUrl: string;
29
- flavor?: OpenAiCompatibleFlavor;
30
- /** Presence-only: empty string/undefined means unconfigured for api-key flavors. */
31
- apiKey?: string;
32
- /**
33
- * Ollama and some local servers ignore auth; when true (default for ollama),
34
- * send Bearer ollama or dummy if no key is set.
35
- */
36
- allowDummyKey?: boolean;
37
- authTier?: AuthTier;
38
- defaultHeaders?: Record<string, string>;
39
- /** Injected fetch for tests. Defaults to globalThis.fetch. */
40
- fetch?: typeof globalThis.fetch;
41
- /** Optional static models for offline listModels. */
42
- models?: ModelCapability[];
43
- /** Opaque limits by default for remote SaaS; ollama is discoverable offline. */
44
- opaqueLimits?: boolean;
45
- estimatedCostUsdPer1kTokens?: number;
46
- /**
47
- * Optional credential chain sources for checkAuth resolution.
48
- * When set, checkAuth will try these sources in order to find a credential.
49
- */
50
- credentialSources?: CredentialSource[];
51
- /**
52
- * Optional secret store for keychain-backed credential resolution.
53
- * Required when credentialSources includes a keychain source.
54
- */
55
- secretStore?: SecretStore;
56
- }
57
-
58
- const OLLAMA_DUMMY_KEY = "ollama";
59
-
60
- function joinUrl(base: string, path: string): string {
61
- const b = base.replace(/\/+$/, "");
62
- const p = path.startsWith("/") ? path : `/${path}`;
63
- return `${b}${p}`;
64
- }
65
-
66
- function headersFor(
67
- options: OpenAiCompatibleOptions,
68
- apiKey: string | undefined,
69
- ): Record<string, string> {
70
- const headers: Record<string, string> = {
71
- "content-type": "application/json",
72
- ...(options.defaultHeaders ?? {}),
73
- };
74
-
75
- const flavor = options.flavor ?? "openai";
76
- const allowDummy = options.allowDummyKey ?? flavor === "ollama";
77
-
78
- let key = apiKey?.trim() || undefined;
79
- if (!key && allowDummy) {
80
- key = OLLAMA_DUMMY_KEY;
81
- }
82
-
83
- if (key) {
84
- headers.authorization = `Bearer ${key}`;
85
- }
86
-
87
- if (flavor === "openrouter") {
88
- // OpenRouter recommended metadata; no secrets.
89
- if (!headers["http-referer"]) {
90
- headers["http-referer"] = "https://offrouter.dev";
91
- }
92
- if (!headers["x-title"]) {
93
- headers["x-title"] = "OffRouter";
94
- }
95
- }
96
-
97
- return headers;
98
- }
99
-
100
- export class OpenAiCompatibleAdapter implements ProviderAdapter {
101
- readonly id: string;
102
- private readonly options: OpenAiCompatibleOptions;
103
- private readonly active = new Map<string, AbortController>();
104
- private readonly cancelled = new Set<string>();
105
-
106
- constructor(options: OpenAiCompatibleOptions) {
107
- this.id = options.id;
108
- this.options = options;
109
- }
110
-
111
- private get fetchImpl(): typeof globalThis.fetch {
112
- return this.options.fetch ?? globalThis.fetch.bind(globalThis);
113
- }
114
-
115
- private offlineNetworkError(operation: string): ProviderError | null {
116
- if (
117
- process.env.OFFROUTER_LIVE_TESTS === "1" ||
118
- this.options.fetch !== undefined
119
- ) {
120
- return null;
121
- }
122
- return new ProviderError({
123
- code: "invalid_request",
124
- message: `${operation} requires injected fetch or OFFROUTER_LIVE_TESTS=1`,
125
- retryable: false,
126
- });
127
- }
128
-
129
- private resolvedAuthTier(): AuthTier {
130
- if (this.options.authTier) return this.options.authTier;
131
- return this.options.flavor === "ollama" ? "local" : "api-key";
132
- }
133
-
134
- private hasConfiguredKey(): boolean {
135
- const key = this.options.apiKey?.trim();
136
- return Boolean(key);
137
- }
138
-
139
- async listModels(): Promise<ModelCapability[]> {
140
- if (this.options.models) {
141
- return this.options.models.map((m) => ({ ...m }));
142
- }
143
-
144
- // Live path only when models not injected.
145
- const offlineError = this.offlineNetworkError("listModels");
146
- if (offlineError) {
147
- throw offlineError;
148
- }
149
-
150
- const res = await this.fetchImpl(joinUrl(this.options.baseUrl, "/models"), {
151
- method: "GET",
152
- headers: headersFor(this.options, this.options.apiKey),
153
- });
154
- if (!res.ok) {
155
- const text = await res.text().catch(() => undefined);
156
- throw mapHttpToProviderError(res.status, text);
157
- }
158
- const json = (await res.json()) as {
159
- data?: Array<{ id: string; owned_by?: string }>;
160
- };
161
- const data = json.data ?? [];
162
- return data.map((m) => ({
163
- modelId: m.id,
164
- displayName: m.id,
165
- supportsTools: true,
166
- supportsStreaming: true,
167
- supportsJson: true,
168
- supportsImages: false,
169
- }));
170
- }
171
-
172
- async checkAuth(): Promise<AuthStatus> {
173
- const tier = this.resolvedAuthTier();
174
- const flavor = this.options.flavor ?? "openai";
175
- const allowDummy = this.options.allowDummyKey ?? flavor === "ollama";
176
- const hasKey = this.hasConfiguredKey();
177
-
178
- // Try credential chain resolution if sources are configured.
179
- let credentialSource: AuthStatus["credentialSource"] = undefined;
180
- let resolvedKey = false;
181
-
182
- if (this.options.credentialSources && this.options.secretStore) {
183
- const { resolveCredential } = await import("../auth/credential-chain.js");
184
- const result = await resolveCredential(this.options.credentialSources, {
185
- store: this.options.secretStore,
186
- });
187
- if (result !== null) {
188
- credentialSource = result.source;
189
- resolvedKey = true;
190
- }
191
- }
192
-
193
- // If credential chain found nothing, check the configured key or dummy.
194
- const configured = tier === "local" ? true : hasKey || resolvedKey || allowDummy;
195
- const finalCredentialSource = credentialSource ?? (hasKey ? "config" : allowDummy ? "none" : "none");
196
-
197
- // OAuth providers are subscription-eligible when tokens are present.
198
- // For api-key providers, subscriptionEligible is always false.
199
- const isOAuthProvider = flavor === "openai" || flavor === "openrouter";
200
- const subscriptionEligible = isOAuthProvider && credentialSource === "oauth";
201
-
202
- return {
203
- authTier: tier,
204
- authScope:
205
- flavor === "openrouter"
206
- ? "third-party"
207
- : flavor === "ollama"
208
- ? "first-party"
209
- : "first-party",
210
- configured,
211
- hasApiKey: hasKey || resolvedKey,
212
- subscriptionStatus: undefined,
213
- limitsDiscoverable: !(this.options.opaqueLimits ?? tier !== "local"),
214
- health: configured ? "healthy" : "dead",
215
- detail: !hasKey && !resolvedKey && allowDummy ? "using_dummy_key" : undefined,
216
- credentialSource: configured ? finalCredentialSource : "none",
217
- subscriptionEligible,
218
- };
219
- }
220
-
221
- async queryLimits(authTier: AuthTier): Promise<LimitSnapshot> {
222
- const opaque =
223
- this.options.opaqueLimits ?? this.resolvedAuthTier() !== "local";
224
- if (opaque) {
225
- return {
226
- authTier,
227
- discoverable: false,
228
- opaque: true,
229
- metadata: {
230
- flavor: this.options.flavor ?? "openai",
231
- note: "limits_not_exposed_by_provider",
232
- },
233
- };
234
- }
235
- return {
236
- authTier,
237
- discoverable: true,
238
- opaque: false,
239
- remaining: undefined,
240
- limit: undefined,
241
- unit: "unknown",
242
- metadata: {
243
- flavor: this.options.flavor ?? "openai",
244
- source: "local_runtime",
245
- },
246
- };
247
- }
248
-
249
- async estimate(
250
- request: RouteRequest,
251
- model: string,
252
- authTier: AuthTier,
253
- ): Promise<CostEstimate> {
254
- const previewLen = request.task.promptPreview.length;
255
- const inputTokensEstimate = Math.max(1, Math.ceil(previewLen / 4));
256
- const per1k = this.options.estimatedCostUsdPer1kTokens ?? 0.001;
257
- const cost =
258
- authTier === "api-key" ? (inputTokensEstimate / 1000) * per1k : 0;
259
- return {
260
- model,
261
- authTier,
262
- estimatedCostUsd: cost,
263
- currency: "USD",
264
- inputTokensEstimate,
265
- note: "heuristic prompt-preview estimate",
266
- };
267
- }
268
-
269
- async *invoke(request: ProviderInvokeRequest): AsyncIterable<ProviderEvent> {
270
- if (this.cancelled.has(request.callId) || request.signal?.aborted) {
271
- yield {
272
- type: "error",
273
- callId: request.callId,
274
- error: new ProviderError({
275
- code: "cancelled",
276
- message: `call ${request.callId} cancelled`,
277
- retryable: false,
278
- }),
279
- };
280
- return;
281
- }
282
-
283
- const offlineError = this.offlineNetworkError("invoke");
284
- if (offlineError) {
285
- yield { type: "error", callId: request.callId, error: offlineError };
286
- return;
287
- }
288
-
289
- const controller = new AbortController();
290
- this.active.set(request.callId, controller);
291
- const onExternalAbort = () => controller.abort();
292
- request.signal?.addEventListener("abort", onExternalAbort, { once: true });
293
-
294
- yield {
295
- type: "start",
296
- callId: request.callId,
297
- model: request.model,
298
- };
299
-
300
- try {
301
- const stream = request.stream ?? false;
302
- const body = {
303
- model: request.model,
304
- messages: request.messages.map((m) => ({
305
- role: m.role,
306
- content: m.content,
307
- })),
308
- stream,
309
- ...(request.maxOutputTokens !== undefined
310
- ? { max_tokens: request.maxOutputTokens }
311
- : {}),
312
- ...(request.temperature !== undefined
313
- ? { temperature: request.temperature }
314
- : {}),
315
- };
316
-
317
- const res = await this.fetchImpl(
318
- joinUrl(this.options.baseUrl, "/chat/completions"),
319
- {
320
- method: "POST",
321
- headers: headersFor(this.options, this.options.apiKey),
322
- body: JSON.stringify(body),
323
- signal: controller.signal,
324
- },
325
- );
326
-
327
- if (!res.ok) {
328
- const text = await res.text().catch(() => undefined);
329
- const err = mapHttpToProviderError(res.status, text);
330
- yield { type: "error", callId: request.callId, error: err };
331
- return;
332
- }
333
-
334
- if (stream) {
335
- if (!res.body) {
336
- yield {
337
- type: "error",
338
- callId: request.callId,
339
- error: new ProviderError({
340
- code: "stream_interrupted",
341
- message: "streaming response missing body",
342
- retryable: true,
343
- }),
344
- };
345
- return;
346
- }
347
- yield* this.readSseStream(request.callId, res.body, controller.signal);
348
- return;
349
- }
350
-
351
- const json = (await res.json()) as {
352
- choices?: Array<{
353
- message?: { content?: string };
354
- finish_reason?: string;
355
- }>;
356
- usage?: {
357
- prompt_tokens?: number;
358
- completion_tokens?: number;
359
- total_tokens?: number;
360
- };
361
- };
362
- const text = json.choices?.[0]?.message?.content ?? "";
363
- if (text) {
364
- yield {
365
- type: "text-delta",
366
- callId: request.callId,
367
- text,
368
- };
369
- }
370
- if (json.usage) {
371
- yield {
372
- type: "usage",
373
- callId: request.callId,
374
- inputTokens: json.usage.prompt_tokens,
375
- outputTokens: json.usage.completion_tokens,
376
- totalTokens: json.usage.total_tokens,
377
- };
378
- }
379
- yield {
380
- type: "done",
381
- callId: request.callId,
382
- finishReason: json.choices?.[0]?.finish_reason ?? "stop",
383
- };
384
- } catch (err) {
385
- if (controller.signal.aborted || this.cancelled.has(request.callId)) {
386
- yield {
387
- type: "error",
388
- callId: request.callId,
389
- error: new ProviderError({
390
- code: "cancelled",
391
- message: `call ${request.callId} cancelled`,
392
- retryable: false,
393
- cause: err,
394
- }),
395
- };
396
- return;
397
- }
398
- if (err instanceof ProviderError) {
399
- yield { type: "error", callId: request.callId, error: err };
400
- return;
401
- }
402
- const message =
403
- err instanceof Error ? err.message : "unknown network error";
404
- yield {
405
- type: "error",
406
- callId: request.callId,
407
- error: new ProviderError({
408
- code: "network",
409
- message,
410
- retryable: true,
411
- cause: err,
412
- }),
413
- };
414
- } finally {
415
- request.signal?.removeEventListener("abort", onExternalAbort);
416
- this.active.delete(request.callId);
417
- }
418
- }
419
-
420
- private async *readSseStream(
421
- callId: string,
422
- body: ReadableStream<Uint8Array>,
423
- signal: AbortSignal,
424
- ): AsyncIterable<ProviderEvent> {
425
- const reader = body.getReader();
426
- const decoder = new TextDecoder();
427
- let buffer = "";
428
- const onAbort = () => {
429
- void reader.cancel().catch(() => undefined);
430
- };
431
- signal.addEventListener("abort", onAbort, { once: true });
432
- try {
433
- while (true) {
434
- if (signal.aborted || this.cancelled.has(callId)) {
435
- yield {
436
- type: "error",
437
- callId,
438
- error: new ProviderError({
439
- code: "cancelled",
440
- message: `call ${callId} cancelled`,
441
- retryable: false,
442
- }),
443
- };
444
- return;
445
- }
446
- const { done, value } = await reader.read();
447
- if (signal.aborted || this.cancelled.has(callId)) {
448
- yield {
449
- type: "error",
450
- callId,
451
- error: new ProviderError({
452
- code: "cancelled",
453
- message: `call ${callId} cancelled`,
454
- retryable: false,
455
- }),
456
- };
457
- return;
458
- }
459
- if (done) break;
460
- buffer += decoder.decode(value, { stream: true });
461
- const parts = buffer.split("\n");
462
- buffer = parts.pop() ?? "";
463
- for (const line of parts) {
464
- const trimmed = line.trim();
465
- if (!trimmed.startsWith("data:")) continue;
466
- const payload = trimmed.slice(5).trim();
467
- if (payload === "[DONE]") {
468
- yield { type: "done", callId, finishReason: "stop" };
469
- return;
470
- }
471
- try {
472
- const json = JSON.parse(payload) as {
473
- choices?: Array<{
474
- delta?: { content?: string };
475
- finish_reason?: string | null;
476
- }>;
477
- usage?: {
478
- prompt_tokens?: number;
479
- completion_tokens?: number;
480
- total_tokens?: number;
481
- };
482
- };
483
- const delta = json.choices?.[0]?.delta?.content;
484
- if (delta) {
485
- yield { type: "text-delta", callId, text: delta };
486
- }
487
- if (json.usage) {
488
- yield {
489
- type: "usage",
490
- callId,
491
- inputTokens: json.usage.prompt_tokens,
492
- outputTokens: json.usage.completion_tokens,
493
- totalTokens: json.usage.total_tokens,
494
- };
495
- }
496
- const finish = json.choices?.[0]?.finish_reason;
497
- if (finish) {
498
- yield { type: "done", callId, finishReason: finish };
499
- return;
500
- }
501
- } catch {
502
- // ignore malformed sse lines
503
- }
504
- }
505
- }
506
- yield { type: "done", callId, finishReason: "stop" };
507
- } catch (err) {
508
- if (signal.aborted || this.cancelled.has(callId)) {
509
- yield {
510
- type: "error",
511
- callId,
512
- error: new ProviderError({
513
- code: "cancelled",
514
- message: `call ${callId} cancelled`,
515
- retryable: false,
516
- cause: err,
517
- }),
518
- };
519
- return;
520
- }
521
- yield {
522
- type: "error",
523
- callId,
524
- error: new ProviderError({
525
- code: "stream_interrupted",
526
- message: err instanceof Error ? err.message : "stream interrupted",
527
- retryable: true,
528
- cause: err,
529
- }),
530
- };
531
- } finally {
532
- signal.removeEventListener("abort", onAbort);
533
- try {
534
- reader.releaseLock();
535
- } catch {
536
- // ignore
537
- }
538
- }
539
- }
540
-
541
- async cancel(callId: string): Promise<void> {
542
- this.cancelled.add(callId);
543
- this.active.get(callId)?.abort();
544
- }
545
- }
546
-
547
- export function createOpenAiCompatibleAdapter(
548
- options: OpenAiCompatibleOptions,
549
- ): OpenAiCompatibleAdapter {
550
- return new OpenAiCompatibleAdapter(options);
551
- }
552
-
553
- /** Exported for tests that assert Ollama dummy key behavior. */
554
- export const OLLAMA_DUMMY_BEARER = `Bearer ${OLLAMA_DUMMY_KEY}`;