@a-dray/aglib 0.1.0

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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/dist/agent.d.ts +158 -0
  4. package/dist/agent.js +2 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/content.d.ts +45 -0
  7. package/dist/content.js +11 -0
  8. package/dist/content.js.map +1 -0
  9. package/dist/harness/adapters/acp/index.d.ts +92 -0
  10. package/dist/harness/adapters/acp/index.js +479 -0
  11. package/dist/harness/adapters/acp/index.js.map +1 -0
  12. package/dist/harness/adapters/acp/rpc.d.ts +23 -0
  13. package/dist/harness/adapters/acp/rpc.js +103 -0
  14. package/dist/harness/adapters/acp/rpc.js.map +1 -0
  15. package/dist/harness/adapters/native/compaction.d.ts +16 -0
  16. package/dist/harness/adapters/native/compaction.js +55 -0
  17. package/dist/harness/adapters/native/compaction.js.map +1 -0
  18. package/dist/harness/adapters/native/index.d.ts +2 -0
  19. package/dist/harness/adapters/native/index.js +2 -0
  20. package/dist/harness/adapters/native/index.js.map +1 -0
  21. package/dist/harness/adapters/native/loop.d.ts +25 -0
  22. package/dist/harness/adapters/native/loop.js +153 -0
  23. package/dist/harness/adapters/native/loop.js.map +1 -0
  24. package/dist/harness/harness.d.ts +129 -0
  25. package/dist/harness/harness.js +2 -0
  26. package/dist/harness/harness.js.map +1 -0
  27. package/dist/harness/index.d.ts +3 -0
  28. package/dist/harness/index.js +2 -0
  29. package/dist/harness/index.js.map +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +5 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/json.d.ts +4 -0
  34. package/dist/json.js +2 -0
  35. package/dist/json.js.map +1 -0
  36. package/dist/model/adapters/anthropic/index.d.ts +34 -0
  37. package/dist/model/adapters/anthropic/index.js +271 -0
  38. package/dist/model/adapters/anthropic/index.js.map +1 -0
  39. package/dist/model/adapters/fake/index.d.ts +27 -0
  40. package/dist/model/adapters/fake/index.js +37 -0
  41. package/dist/model/adapters/fake/index.js.map +1 -0
  42. package/dist/model/adapters/openai-compatible/index.d.ts +29 -0
  43. package/dist/model/adapters/openai-compatible/index.js +226 -0
  44. package/dist/model/adapters/openai-compatible/index.js.map +1 -0
  45. package/dist/model/conformance.d.ts +210 -0
  46. package/dist/model/conformance.js +427 -0
  47. package/dist/model/conformance.js.map +1 -0
  48. package/dist/model/index.d.ts +2 -0
  49. package/dist/model/index.js +2 -0
  50. package/dist/model/index.js.map +1 -0
  51. package/dist/model/model.d.ts +85 -0
  52. package/dist/model/model.js +8 -0
  53. package/dist/model/model.js.map +1 -0
  54. package/dist/result.d.ts +23 -0
  55. package/dist/result.js +3 -0
  56. package/dist/result.js.map +1 -0
  57. package/dist/run.d.ts +10 -0
  58. package/dist/run.js +477 -0
  59. package/dist/run.js.map +1 -0
  60. package/dist/sandbox/adapters/docker.d.ts +22 -0
  61. package/dist/sandbox/adapters/docker.js +443 -0
  62. package/dist/sandbox/adapters/docker.js.map +1 -0
  63. package/dist/sandbox/adapters/local.d.ts +11 -0
  64. package/dist/sandbox/adapters/local.js +284 -0
  65. package/dist/sandbox/adapters/local.js.map +1 -0
  66. package/dist/sandbox/conformance.d.ts +73 -0
  67. package/dist/sandbox/conformance.js +309 -0
  68. package/dist/sandbox/conformance.js.map +1 -0
  69. package/dist/sandbox/index.d.ts +1 -0
  70. package/dist/sandbox/index.js +2 -0
  71. package/dist/sandbox/index.js.map +1 -0
  72. package/dist/sandbox/sandbox.d.ts +172 -0
  73. package/dist/sandbox/sandbox.js +2 -0
  74. package/dist/sandbox/sandbox.js.map +1 -0
  75. package/dist/session/entry.d.ts +196 -0
  76. package/dist/session/entry.js +2 -0
  77. package/dist/session/entry.js.map +1 -0
  78. package/dist/session/index.d.ts +2 -0
  79. package/dist/session/index.js +2 -0
  80. package/dist/session/index.js.map +1 -0
  81. package/dist/session/log.d.ts +20 -0
  82. package/dist/session/log.js +24 -0
  83. package/dist/session/log.js.map +1 -0
  84. package/dist/session/messages.d.ts +42 -0
  85. package/dist/session/messages.js +99 -0
  86. package/dist/session/messages.js.map +1 -0
  87. package/dist/store/adapters/sqlite.d.ts +20 -0
  88. package/dist/store/adapters/sqlite.js +365 -0
  89. package/dist/store/adapters/sqlite.js.map +1 -0
  90. package/dist/store/conformance.d.ts +66 -0
  91. package/dist/store/conformance.js +580 -0
  92. package/dist/store/conformance.js.map +1 -0
  93. package/dist/store/index.d.ts +1 -0
  94. package/dist/store/index.js +2 -0
  95. package/dist/store/index.js.map +1 -0
  96. package/dist/store/store.d.ts +244 -0
  97. package/dist/store/store.js +2 -0
  98. package/dist/store/store.js.map +1 -0
  99. package/dist/tools/execute.d.ts +24 -0
  100. package/dist/tools/execute.js +105 -0
  101. package/dist/tools/execute.js.map +1 -0
  102. package/dist/tools/index.d.ts +2 -0
  103. package/dist/tools/index.js +2 -0
  104. package/dist/tools/index.js.map +1 -0
  105. package/dist/tools/tool.d.ts +106 -0
  106. package/dist/tools/tool.js +34 -0
  107. package/dist/tools/tool.js.map +1 -0
  108. package/package.json +119 -0
@@ -0,0 +1,226 @@
1
+ import { err, ok } from "../../../result.js";
2
+ import { textOf } from "../../../content.js";
3
+ /**
4
+ * The OpenAI chat-completions wire, which almost every provider now speaks —
5
+ * OpenRouter, Together, Groq, vLLM, Ollama. One adapter, one base URL.
6
+ */
7
+ export function createOpenAiCompatibleModel(options) {
8
+ const call = options.fetch ?? fetch;
9
+ return {
10
+ id: `openai-compatible:${options.model}`,
11
+ async *generate(request) {
12
+ let response;
13
+ try {
14
+ response = await call(`${options.baseUrl}/chat/completions`, {
15
+ method: "POST",
16
+ headers: {
17
+ "content-type": "application/json",
18
+ authorization: `Bearer ${options.apiKey}`,
19
+ ...options.headers,
20
+ },
21
+ body: JSON.stringify({
22
+ model: options.model,
23
+ messages: request.messages.map(encodeMessage),
24
+ ...(request.tools?.length ? { tools: request.tools.map(encodeTool) } : {}),
25
+ ...(request.maxOutputTokens !== undefined ? { max_tokens: request.maxOutputTokens } : {}),
26
+ ...(request.temperature !== undefined ? { temperature: request.temperature } : {}),
27
+ ...encodeEffort(request.effort, options.effortParameter ?? "reasoning_effort"),
28
+ stream: true,
29
+ stream_options: { include_usage: true },
30
+ }),
31
+ ...(request.signal ? { signal: request.signal } : {}),
32
+ });
33
+ }
34
+ catch (error) {
35
+ return err(transportError(error, request.signal));
36
+ }
37
+ if (!response.ok)
38
+ return err(await httpError(response));
39
+ if (!response.body)
40
+ return err({ code: "provider", message: "No response body", retryable: true });
41
+ const text = [];
42
+ const calls = new Map();
43
+ let finish = "stop";
44
+ let usage = {};
45
+ let model;
46
+ // A stream that dies mid-body — cancelled, dropped, truncated — must come
47
+ // back as a typed failure like any other. Without this the generator throws,
48
+ // `run.result` rejects instead of resolving `cancelled`, no `run.finished`
49
+ // is ever committed, and the activation stays open until its claim expires.
50
+ try {
51
+ for await (const event of sseLines(response.body)) {
52
+ if (event === "[DONE]")
53
+ break;
54
+ let frame;
55
+ try {
56
+ frame = JSON.parse(event);
57
+ }
58
+ catch {
59
+ continue;
60
+ }
61
+ model ??= frame.model;
62
+ if (frame.usage)
63
+ usage = decodeUsage(frame.usage);
64
+ const choice = frame.choices?.[0];
65
+ if (!choice)
66
+ continue;
67
+ if (choice.finish_reason)
68
+ finish = decodeFinish(choice.finish_reason);
69
+ const content = choice.delta?.content;
70
+ if (content) {
71
+ text.push(content);
72
+ yield { type: "text.delta", text: content };
73
+ }
74
+ const reasoning = choice.delta?.reasoning;
75
+ if (reasoning)
76
+ yield { type: "reasoning.delta", text: reasoning };
77
+ for (const fragment of choice.delta?.tool_calls ?? []) {
78
+ // Providers stream tool arguments in fragments, keyed by position;
79
+ // only the first fragment carries the id and the name.
80
+ const existing = calls.get(fragment.index) ?? {
81
+ callId: fragment.id ?? `call_${fragment.index}`,
82
+ name: fragment.function?.name ?? "",
83
+ arguments: "",
84
+ };
85
+ if (fragment.id)
86
+ existing.callId = fragment.id;
87
+ if (fragment.function?.name)
88
+ existing.name = fragment.function.name;
89
+ if (fragment.function?.arguments) {
90
+ existing.arguments += fragment.function.arguments;
91
+ yield { type: "tool-call.delta", callId: existing.callId, arguments: fragment.function.arguments };
92
+ }
93
+ calls.set(fragment.index, existing);
94
+ }
95
+ }
96
+ }
97
+ catch (error) {
98
+ return err(transportError(error, request.signal));
99
+ }
100
+ const collected = [...calls.values()]
101
+ .map((call) => ({ callId: call.callId, name: call.name, arguments: call.arguments || "{}" }));
102
+ return ok({
103
+ message: { content: text.join(""), ...(collected.length ? { calls: collected } : {}) },
104
+ finishReason: collected.length && finish === "stop" ? "tool-calls" : finish,
105
+ usage,
106
+ ...(model ? { model } : {}),
107
+ });
108
+ },
109
+ };
110
+ }
111
+ /** OpenRouter is this wire with a fixed base URL and attribution headers. */
112
+ export function createOpenRouterModel(input) {
113
+ return createOpenAiCompatibleModel({
114
+ apiKey: input.apiKey,
115
+ baseUrl: "https://openrouter.ai/api/v1",
116
+ model: input.model,
117
+ effortParameter: "reasoning",
118
+ headers: {
119
+ ...(input.appUrl ? { "http-referer": input.appUrl } : {}),
120
+ ...(input.appName ? { "x-title": input.appName } : {}),
121
+ },
122
+ ...(input.fetch ? { fetch: input.fetch } : {}),
123
+ });
124
+ }
125
+ function encodeEffort(effort, parameter) {
126
+ if (!effort || parameter === "none")
127
+ return {};
128
+ return parameter === "reasoning" ? { reasoning: { effort } } : { reasoning_effort: effort };
129
+ }
130
+ function encodeMessage(message) {
131
+ if (message.role === "tool") {
132
+ return { role: "tool", tool_call_id: message.callId, content: textOf(message.content) };
133
+ }
134
+ if (message.role === "assistant") {
135
+ return {
136
+ role: "assistant",
137
+ content: textOf(message.content) || null,
138
+ ...(message.calls?.length
139
+ ? {
140
+ tool_calls: message.calls.map((call) => ({
141
+ id: call.callId, type: "function",
142
+ function: { name: call.name, arguments: call.arguments },
143
+ })),
144
+ }
145
+ : {}),
146
+ };
147
+ }
148
+ return { role: message.role, content: encodeContent(message.content) };
149
+ }
150
+ function encodeContent(content) {
151
+ if (typeof content === "string")
152
+ return content;
153
+ const parts = [];
154
+ for (const part of content) {
155
+ if (part.type === "text") {
156
+ parts.push({ type: "text", text: part.text });
157
+ continue;
158
+ }
159
+ if (part.type === "image") {
160
+ const url = part.source.kind === "url" ? part.source.url : `data:${part.mediaType};base64,${part.source.data}`;
161
+ parts.push({ type: "image_url", image_url: { url } });
162
+ }
163
+ // Files and opaque blocks have no place on this wire. Dropping them beats
164
+ // stringifying them into something the model would read as prose.
165
+ }
166
+ const only = parts.length === 1 ? parts[0] : undefined;
167
+ return only?.type === "text" ? only.text : parts;
168
+ }
169
+ const encodeTool = (tool) => ({
170
+ type: "function",
171
+ function: { name: tool.name, description: tool.description, parameters: tool.parameters },
172
+ });
173
+ const decodeFinish = (reason) => reason === "tool_calls" ? "tool-calls"
174
+ : reason === "length" ? "length"
175
+ : reason === "content_filter" ? "refusal"
176
+ : "stop";
177
+ const decodeUsage = (usage) => ({
178
+ // Carried, not computed. A router picks an upstream provider per request and
179
+ // adds its own margin, so what it charged is a thing only it can say — and
180
+ // the frame carrying it is the one this adapter already reads.
181
+ ...(typeof usage.cost === "number" ? { costUsd: usage.cost } : {}),
182
+ // `prompt_tokens` is the whole prompt and `cached_tokens` is counted inside
183
+ // it — "cached tokens present in the prompt". `Usage` keeps the three input
184
+ // counts disjoint, so the cached part comes out here rather than every
185
+ // caller having to know which wire produced the number it is holding.
186
+ ...(usage.prompt_tokens !== undefined
187
+ ? { inputTokens: Math.max(usage.prompt_tokens - (usage.prompt_tokens_details?.cached_tokens ?? 0), 0) } : {}),
188
+ ...(usage.completion_tokens !== undefined ? { outputTokens: usage.completion_tokens } : {}),
189
+ ...(usage.prompt_tokens_details?.cached_tokens !== undefined
190
+ ? { cacheReadTokens: usage.prompt_tokens_details.cached_tokens } : {}),
191
+ });
192
+ async function httpError(response) {
193
+ const body = await response.text().catch(() => "");
194
+ const code = response.status === 401 || response.status === 403 ? "auth"
195
+ : response.status === 429 ? "rate-limit"
196
+ : response.status === 400 && /context|token/i.test(body) ? "context-length"
197
+ : response.status >= 500 ? "provider"
198
+ : "failed";
199
+ return {
200
+ code,
201
+ message: `${response.status} ${response.statusText}${body ? `: ${body.slice(0, 400)}` : ""}`,
202
+ retryable: code === "rate-limit" || code === "provider",
203
+ };
204
+ }
205
+ function transportError(error, signal) {
206
+ if (signal?.aborted)
207
+ return { code: "cancelled", message: "Generation cancelled", retryable: false };
208
+ return { code: "provider", message: error instanceof Error ? error.message : String(error), retryable: true };
209
+ }
210
+ /** Server-sent events, reassembled across chunk boundaries. */
211
+ async function* sseLines(body) {
212
+ const decoder = new TextDecoder();
213
+ let buffer = "";
214
+ for await (const chunk of body) {
215
+ buffer += decoder.decode(chunk, { stream: true });
216
+ let newline = buffer.indexOf("\n");
217
+ while (newline >= 0) {
218
+ const line = buffer.slice(0, newline).trim();
219
+ buffer = buffer.slice(newline + 1);
220
+ if (line.startsWith("data:"))
221
+ yield line.slice(5).trim();
222
+ newline = buffer.indexOf("\n");
223
+ }
224
+ }
225
+ }
226
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/model/adapters/openai-compatible/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAe,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAkB7C;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,OAAgC;IAC1E,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC;IACpC,OAAO;QACL,EAAE,EAAE,qBAAqB,OAAO,CAAC,KAAK,EAAE;QAExC,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAqB;YACnC,IAAI,QAAkB,CAAC;YACvB,IAAI,CAAC;gBACH,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,OAAO,CAAC,OAAO,mBAAmB,EAAE;oBAC3D,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE;wBACP,cAAc,EAAE,kBAAkB;wBAClC,aAAa,EAAE,UAAU,OAAO,CAAC,MAAM,EAAE;wBACzC,GAAG,OAAO,CAAC,OAAO;qBACnB;oBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,KAAK,EAAE,OAAO,CAAC,KAAK;wBACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC;wBAC7C,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBAC1E,GAAG,CAAC,OAAO,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBACzF,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBAClF,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,eAAe,IAAI,kBAAkB,CAAC;wBAC9E,MAAM,EAAE,IAAI;wBACZ,cAAc,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE;qBACxC,CAAC;oBACF,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACtD,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,GAAG,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;YACpD,CAAC;YAED,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,OAAO,GAAG,CAAC,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;YACxD,IAAI,CAAC,QAAQ,CAAC,IAAI;gBAAE,OAAO,GAAG,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAEnG,MAAM,IAAI,GAAa,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+D,CAAC;YACrF,IAAI,MAAM,GAAkC,MAAM,CAAC;YACnD,IAAI,KAAK,GAAU,EAAE,CAAC;YACtB,IAAI,KAAyB,CAAC;YAE9B,0EAA0E;YAC1E,6EAA6E;YAC7E,2EAA2E;YAC3E,4EAA4E;YAC5E,IAAI,CAAC;gBACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;oBAClD,IAAI,KAAK,KAAK,QAAQ;wBAAE,MAAM;oBAC9B,IAAI,KAAgB,CAAC;oBACrB,IAAI,CAAC;wBAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAc,CAAC;oBAAC,CAAC;oBAAC,MAAM,CAAC;wBAAC,SAAS;oBAAC,CAAC;oBACnE,KAAK,KAAK,KAAK,CAAC,KAAK,CAAC;oBACtB,IAAI,KAAK,CAAC,KAAK;wBAAE,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBAElD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;oBAClC,IAAI,CAAC,MAAM;wBAAE,SAAS;oBACtB,IAAI,MAAM,CAAC,aAAa;wBAAE,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;oBAEtE,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC;oBACtC,IAAI,OAAO,EAAE,CAAC;wBAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;wBAAC,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;oBAAC,CAAC;oBAEjF,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC;oBAC1C,IAAI,SAAS;wBAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;oBAElE,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE,EAAE,CAAC;wBACtD,mEAAmE;wBACnE,uDAAuD;wBACvD,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI;4BAC5C,MAAM,EAAE,QAAQ,CAAC,EAAE,IAAI,QAAQ,QAAQ,CAAC,KAAK,EAAE;4BAC/C,IAAI,EAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE;4BACnC,SAAS,EAAE,EAAE;yBACd,CAAC;wBACF,IAAI,QAAQ,CAAC,EAAE;4BAAE,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,EAAE,CAAC;wBAC/C,IAAI,QAAQ,CAAC,QAAQ,EAAE,IAAI;4BAAE,QAAQ,CAAC,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;wBACpE,IAAI,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC;4BACjC,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC;4BAClD,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;wBACrG,CAAC;wBACD,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;oBACtC,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,GAAG,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;YACpD,CAAC;YAED,MAAM,SAAS,GAAe,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;iBAC9C,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YAEhG,OAAO,EAAE,CAAC;gBACR,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;gBACtF,YAAY,EAAE,SAAS,CAAC,MAAM,IAAI,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM;gBAC3E,KAAK;gBACL,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5B,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,qBAAqB,CAAC,KAErC;IACC,OAAO,2BAA2B,CAAC;QACjC,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,OAAO,EAAE,8BAA8B;QACvC,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,eAAe,EAAE,WAAW;QAC5B,OAAO,EAAE;YACP,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzD,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvD;QACD,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/C,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY,CACnB,MAA8B,EAC9B,SAAoD;IAEpD,IAAI,CAAC,MAAM,IAAI,SAAS,KAAK,MAAM;QAAE,OAAO,EAAE,CAAC;IAC/C,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC;AAC9F,CAAC;AAED,SAAS,aAAa,CAAC,OAAgB;IACrC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1F,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACjC,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI;YACxC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM;gBACvB,CAAC,CAAC;oBACE,UAAU,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;wBACvC,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,UAAU;wBACjC,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;qBACzD,CAAC,CAAC;iBACJ;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;AACzE,CAAC;AAED,SAAS,aAAa,CAAC,OAA2B;IAChD,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IAChD,MAAM,KAAK,GAA8B,EAAE,CAAC;IAC5C,KAAK,MAAM,IAAI,IAAI,OAAiC,EAAE,CAAC;QACrD,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YAAC,SAAS;QAAC,CAAC;QACtF,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,SAAS,WAAW,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC/G,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,0EAA0E;QAC1E,kEAAkE;IACpE,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACvD,OAAO,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,IAAc,EAAE,EAAE,CAAC,CAAC;IACtC,IAAI,EAAE,UAAU;IAChB,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE;CAC1F,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,CAAC,MAAc,EAAiC,EAAE,CACrE,MAAM,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY;IACtC,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ;QAChC,CAAC,CAAC,MAAM,KAAK,gBAAgB,CAAC,CAAC,CAAC,SAAS;YACzC,CAAC,CAAC,MAAM,CAAC;AAEX,MAAM,WAAW,GAAG,CAAC,KAAgB,EAAS,EAAE,CAAC,CAAC;IAChD,6EAA6E;IAC7E,2EAA2E;IAC3E,+DAA+D;IAC/D,GAAG,CAAC,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,4EAA4E;IAC5E,4EAA4E;IAC5E,uEAAuE;IACvE,sEAAsE;IACtE,GAAG,CAAC,KAAK,CAAC,aAAa,KAAK,SAAS;QACnC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,GAAG,CAAC,KAAK,CAAC,qBAAqB,EAAE,aAAa,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/G,GAAG,CAAC,KAAK,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3F,GAAG,CAAC,KAAK,CAAC,qBAAqB,EAAE,aAAa,KAAK,SAAS;QAC1D,CAAC,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,qBAAqB,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;CACzE,CAAC,CAAC;AAEH,KAAK,UAAU,SAAS,CAAC,QAAkB;IACzC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IACnD,MAAM,IAAI,GACR,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM;QAC3D,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,YAAY;YACxC,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,gBAAgB;gBAC3E,CAAC,CAAC,QAAQ,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,UAAU;oBACrC,CAAC,CAAC,QAAQ,CAAC;IACb,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;QAC5F,SAAS,EAAE,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,UAAU;KACxD,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAAc,EAAE,MAAoB;IAC1D,IAAI,MAAM,EAAE,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACrG,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAChH,CAAC;AAED,+DAA+D;AAC/D,KAAK,SAAS,CAAC,CAAC,QAAQ,CAAC,IAAgC;IACvD,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,IAA4C,EAAE,CAAC;QACvE,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,IAAI,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACnC,OAAO,OAAO,IAAI,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;YAC7C,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;YACnC,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;gBAAE,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACzD,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import type {\n ContentPart, Message, Model, ModelDelta, ModelError, ModelRequest, ModelResponse, ToolCall, ToolSpec, Usage,\n} from \"../../model.js\";\nimport { err, ok, type Result } from \"../../../result.js\";\nimport { textOf } from \"../../../content.js\";\n\nexport interface OpenAiCompatibleOptions {\n apiKey: string;\n baseUrl: string;\n model: string;\n headers?: Readonly<Record<string, string>>;\n /**\n * How this endpoint spells reasoning effort. OpenAI takes `reasoning_effort`;\n * OpenRouter takes `reasoning: { effort }` and forwards it to whoever is\n * serving the model. Sending both is not compatibility — OpenAI rejects the\n * request outright — so the dialect is declared rather than guessed.\n */\n effortParameter?: \"reasoning_effort\" | \"reasoning\" | \"none\";\n /** Injectable for tests; defaults to global fetch. */\n fetch?: typeof fetch;\n}\n\n/**\n * The OpenAI chat-completions wire, which almost every provider now speaks —\n * OpenRouter, Together, Groq, vLLM, Ollama. One adapter, one base URL.\n */\nexport function createOpenAiCompatibleModel(options: OpenAiCompatibleOptions): Model {\n const call = options.fetch ?? fetch;\n return {\n id: `openai-compatible:${options.model}`,\n\n async *generate(request: ModelRequest): AsyncGenerator<ModelDelta, Result<ModelResponse, ModelError>> {\n let response: Response;\n try {\n response = await call(`${options.baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${options.apiKey}`,\n ...options.headers,\n },\n body: JSON.stringify({\n model: options.model,\n messages: request.messages.map(encodeMessage),\n ...(request.tools?.length ? { tools: request.tools.map(encodeTool) } : {}),\n ...(request.maxOutputTokens !== undefined ? { max_tokens: request.maxOutputTokens } : {}),\n ...(request.temperature !== undefined ? { temperature: request.temperature } : {}),\n ...encodeEffort(request.effort, options.effortParameter ?? \"reasoning_effort\"),\n stream: true,\n stream_options: { include_usage: true },\n }),\n ...(request.signal ? { signal: request.signal } : {}),\n });\n } catch (error) {\n return err(transportError(error, request.signal));\n }\n\n if (!response.ok) return err(await httpError(response));\n if (!response.body) return err({ code: \"provider\", message: \"No response body\", retryable: true });\n\n const text: string[] = [];\n const calls = new Map<number, { callId: string; name: string; arguments: string }>();\n let finish: ModelResponse[\"finishReason\"] = \"stop\";\n let usage: Usage = {};\n let model: string | undefined;\n\n // A stream that dies mid-body — cancelled, dropped, truncated — must come\n // back as a typed failure like any other. Without this the generator throws,\n // `run.result` rejects instead of resolving `cancelled`, no `run.finished`\n // is ever committed, and the activation stays open until its claim expires.\n try {\n for await (const event of sseLines(response.body)) {\n if (event === \"[DONE]\") break;\n let frame: ChatFrame;\n try { frame = JSON.parse(event) as ChatFrame; } catch { continue; }\n model ??= frame.model;\n if (frame.usage) usage = decodeUsage(frame.usage);\n\n const choice = frame.choices?.[0];\n if (!choice) continue;\n if (choice.finish_reason) finish = decodeFinish(choice.finish_reason);\n\n const content = choice.delta?.content;\n if (content) { text.push(content); yield { type: \"text.delta\", text: content }; }\n\n const reasoning = choice.delta?.reasoning;\n if (reasoning) yield { type: \"reasoning.delta\", text: reasoning };\n\n for (const fragment of choice.delta?.tool_calls ?? []) {\n // Providers stream tool arguments in fragments, keyed by position;\n // only the first fragment carries the id and the name.\n const existing = calls.get(fragment.index) ?? {\n callId: fragment.id ?? `call_${fragment.index}`,\n name: fragment.function?.name ?? \"\",\n arguments: \"\",\n };\n if (fragment.id) existing.callId = fragment.id;\n if (fragment.function?.name) existing.name = fragment.function.name;\n if (fragment.function?.arguments) {\n existing.arguments += fragment.function.arguments;\n yield { type: \"tool-call.delta\", callId: existing.callId, arguments: fragment.function.arguments };\n }\n calls.set(fragment.index, existing);\n }\n }\n } catch (error) {\n return err(transportError(error, request.signal));\n }\n\n const collected: ToolCall[] = [...calls.values()]\n .map((call) => ({ callId: call.callId, name: call.name, arguments: call.arguments || \"{}\" }));\n\n return ok({\n message: { content: text.join(\"\"), ...(collected.length ? { calls: collected } : {}) },\n finishReason: collected.length && finish === \"stop\" ? \"tool-calls\" : finish,\n usage,\n ...(model ? { model } : {}),\n });\n },\n };\n}\n\n/** OpenRouter is this wire with a fixed base URL and attribution headers. */\nexport function createOpenRouterModel(input: {\n apiKey: string; model: string; appUrl?: string; appName?: string; fetch?: typeof fetch;\n}): Model {\n return createOpenAiCompatibleModel({\n apiKey: input.apiKey,\n baseUrl: \"https://openrouter.ai/api/v1\",\n model: input.model,\n effortParameter: \"reasoning\",\n headers: {\n ...(input.appUrl ? { \"http-referer\": input.appUrl } : {}),\n ...(input.appName ? { \"x-title\": input.appName } : {}),\n },\n ...(input.fetch ? { fetch: input.fetch } : {}),\n });\n}\n\nfunction encodeEffort(\n effort: ModelRequest[\"effort\"],\n parameter: \"reasoning_effort\" | \"reasoning\" | \"none\",\n): Record<string, unknown> {\n if (!effort || parameter === \"none\") return {};\n return parameter === \"reasoning\" ? { reasoning: { effort } } : { reasoning_effort: effort };\n}\n\nfunction encodeMessage(message: Message): Record<string, unknown> {\n if (message.role === \"tool\") {\n return { role: \"tool\", tool_call_id: message.callId, content: textOf(message.content) };\n }\n if (message.role === \"assistant\") {\n return {\n role: \"assistant\",\n content: textOf(message.content) || null,\n ...(message.calls?.length\n ? {\n tool_calls: message.calls.map((call) => ({\n id: call.callId, type: \"function\",\n function: { name: call.name, arguments: call.arguments },\n })),\n }\n : {}),\n };\n }\n return { role: message.role, content: encodeContent(message.content) };\n}\n\nfunction encodeContent(content: Message[\"content\"]): unknown {\n if (typeof content === \"string\") return content;\n const parts: Record<string, unknown>[] = [];\n for (const part of content as readonly ContentPart[]) {\n if (part.type === \"text\") { parts.push({ type: \"text\", text: part.text }); continue; }\n if (part.type === \"image\") {\n const url = part.source.kind === \"url\" ? part.source.url : `data:${part.mediaType};base64,${part.source.data}`;\n parts.push({ type: \"image_url\", image_url: { url } });\n }\n // Files and opaque blocks have no place on this wire. Dropping them beats\n // stringifying them into something the model would read as prose.\n }\n const only = parts.length === 1 ? parts[0] : undefined;\n return only?.type === \"text\" ? only.text : parts;\n}\n\nconst encodeTool = (tool: ToolSpec) => ({\n type: \"function\",\n function: { name: tool.name, description: tool.description, parameters: tool.parameters },\n});\n\nconst decodeFinish = (reason: string): ModelResponse[\"finishReason\"] =>\n reason === \"tool_calls\" ? \"tool-calls\"\n : reason === \"length\" ? \"length\"\n : reason === \"content_filter\" ? \"refusal\"\n : \"stop\";\n\nconst decodeUsage = (usage: ChatUsage): Usage => ({\n // Carried, not computed. A router picks an upstream provider per request and\n // adds its own margin, so what it charged is a thing only it can say — and\n // the frame carrying it is the one this adapter already reads.\n ...(typeof usage.cost === \"number\" ? { costUsd: usage.cost } : {}),\n // `prompt_tokens` is the whole prompt and `cached_tokens` is counted inside\n // it — \"cached tokens present in the prompt\". `Usage` keeps the three input\n // counts disjoint, so the cached part comes out here rather than every\n // caller having to know which wire produced the number it is holding.\n ...(usage.prompt_tokens !== undefined\n ? { inputTokens: Math.max(usage.prompt_tokens - (usage.prompt_tokens_details?.cached_tokens ?? 0), 0) } : {}),\n ...(usage.completion_tokens !== undefined ? { outputTokens: usage.completion_tokens } : {}),\n ...(usage.prompt_tokens_details?.cached_tokens !== undefined\n ? { cacheReadTokens: usage.prompt_tokens_details.cached_tokens } : {}),\n});\n\nasync function httpError(response: Response): Promise<ModelError> {\n const body = await response.text().catch(() => \"\");\n const code: ModelError[\"code\"] =\n response.status === 401 || response.status === 403 ? \"auth\"\n : response.status === 429 ? \"rate-limit\"\n : response.status === 400 && /context|token/i.test(body) ? \"context-length\"\n : response.status >= 500 ? \"provider\"\n : \"failed\";\n return {\n code,\n message: `${response.status} ${response.statusText}${body ? `: ${body.slice(0, 400)}` : \"\"}`,\n retryable: code === \"rate-limit\" || code === \"provider\",\n };\n}\n\nfunction transportError(error: unknown, signal?: AbortSignal): ModelError {\n if (signal?.aborted) return { code: \"cancelled\", message: \"Generation cancelled\", retryable: false };\n return { code: \"provider\", message: error instanceof Error ? error.message : String(error), retryable: true };\n}\n\n/** Server-sent events, reassembled across chunk boundaries. */\nasync function* sseLines(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {\n const decoder = new TextDecoder();\n let buffer = \"\";\n for await (const chunk of body as unknown as AsyncIterable<Uint8Array>) {\n buffer += decoder.decode(chunk, { stream: true });\n let newline = buffer.indexOf(\"\\n\");\n while (newline >= 0) {\n const line = buffer.slice(0, newline).trim();\n buffer = buffer.slice(newline + 1);\n if (line.startsWith(\"data:\")) yield line.slice(5).trim();\n newline = buffer.indexOf(\"\\n\");\n }\n }\n}\n\ninterface ChatUsage {\n prompt_tokens?: number;\n completion_tokens?: number;\n prompt_tokens_details?: { cached_tokens?: number };\n /**\n * What this generation was charged. OpenRouter sets it on every response and\n * calls it credits, whose base currency is US dollars; plain OpenAI and the\n * self-hosted endpoints on this wire do not send it at all, so it is absent\n * rather than zero there.\n */\n cost?: number;\n}\ninterface ChatFrame {\n model?: string;\n usage?: ChatUsage;\n choices?: readonly {\n finish_reason?: string | null;\n delta?: {\n content?: string;\n reasoning?: string;\n tool_calls?: readonly { index: number; id?: string; function?: { name?: string; arguments?: string } }[];\n };\n }[];\n}\n"]}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * What any implementation of the model port must do.
3
+ *
4
+ * The port is one method, and almost everything it promises is about the shape
5
+ * of an answer rather than its content: exactly one result and nothing after it,
6
+ * a cancellation that arrives as a value, deltas that add up to the message,
7
+ * tool arguments that parse, counts that stay absent when nobody reported them.
8
+ * Those are the promises a run recovers on — and every one of them is invisible
9
+ * to a test that only asks a model a question and reads the reply.
10
+ *
11
+ * So the suite cannot supply requests and expect responses: what a provider says
12
+ * is the provider's, and two of the three implementations here reach one. What
13
+ * a subject is asked for instead is a model **primed to say a scripted thing** —
14
+ * the script in the port's own vocabulary, rendered by the subject onto whatever
15
+ * it actually speaks, which for a wire is a stream of frames and for a fake is a
16
+ * scripted response. A case then drives that model and holds the answer to the
17
+ * port, without ever naming a provider's fields.
18
+ *
19
+ * The same trick runs the other way. An adapter that decodes a response
20
+ * perfectly and never transmits the caller's tools is useless, and that half of
21
+ * an adapter is pure encoding — which is why a wire also hands back a
22
+ * `SentRequest`: its own translation of what went out, back into the port's
23
+ * vocabulary. The alternative was to assert on the raw body, which would make
24
+ * every case provider-specific and so not a shared contract at all.
25
+ *
26
+ * One request per model. Nothing in this port is a conversation — a `Model` is a
27
+ * value you call — so a case wanting a second answer asks for a second model,
28
+ * and no subject has to decide what a spent script means.
29
+ *
30
+ * Inert on purpose. Each case is a name and a function that throws, so the suite
31
+ * drags no test framework into the package:
32
+ *
33
+ * ```ts
34
+ * for (const item of defineModelConformance(subject)) test(item.name, item.run);
35
+ * ```
36
+ */
37
+ import type { Model, ToolCall, Usage } from "./model.js";
38
+ import type { JsonValue } from "../json.js";
39
+ /** One case: a name, and a function that throws when the contract is broken. */
40
+ export interface ConformanceCase {
41
+ name: string;
42
+ run(): Promise<void>;
43
+ }
44
+ /**
45
+ * What the provider should say, in the port's vocabulary.
46
+ *
47
+ * A subject renders each of these onto whatever it speaks. Two rendering rules
48
+ * carry cases of their own, so they are stated here rather than assumed:
49
+ *
50
+ * - **Deltas arrive one at a time.** A subject that hands the whole body over at
51
+ * once cannot be cancelled part way through, and the case that pulls one delta
52
+ * and then gives up would prove nothing.
53
+ * - **A wire behaves as `fetch` does about the signal.** An already-aborted
54
+ * request never reaches the provider, and a body dies when the caller gives
55
+ * up. A transport that ignores the signal lets an adapter that never forwarded
56
+ * it pass both cancellation cases.
57
+ */
58
+ export type ModelScript =
59
+ /** An answer that arrives in pieces. */
60
+ {
61
+ kind: "text";
62
+ deltas: readonly string[];
63
+ }
64
+ /** An answer the model thought about first, aloud. */
65
+ | {
66
+ kind: "reasoning";
67
+ thoughts: readonly string[];
68
+ text: string;
69
+ }
70
+ /** A turn that asks for tools, with each call's arguments fragmented on the way. */
71
+ | {
72
+ kind: "tool-calls";
73
+ calls: readonly ToolCall[];
74
+ }
75
+ /** An answer the provider cut off at the output ceiling, mid-sentence or mid-call. */
76
+ | {
77
+ kind: "truncated";
78
+ text: string;
79
+ calls?: readonly ToolCall[];
80
+ }
81
+ /** An answer the provider declined to give. */
82
+ | {
83
+ kind: "refused";
84
+ }
85
+ /** An answer reporting exactly these counts, from exactly this model. */
86
+ | {
87
+ kind: "usage";
88
+ usage: Usage;
89
+ model: string;
90
+ }
91
+ /** An answer that says nothing about counts or about which model served it. */
92
+ | {
93
+ kind: "silent";
94
+ }
95
+ /** The provider refusing the request outright. Wires only. */
96
+ | {
97
+ kind: "status";
98
+ status: number;
99
+ body: string;
100
+ }
101
+ /** A body that delivers this much and then dies. Wires only. */
102
+ | {
103
+ kind: "cut";
104
+ text: string;
105
+ };
106
+ /**
107
+ * What one outgoing request carried, translated back out of the wire's own
108
+ * vocabulary by the subject. Absence is meaningful: an empty `toolNames`
109
+ * asserts the request advertised no tools, and a missing `effort` asserts it
110
+ * carried no reasoning control at all.
111
+ */
112
+ export interface SentRequest {
113
+ /**
114
+ * Every text the request transmitted for the model to read, in order —
115
+ * instructions first, then the conversation, wherever this wire puts them.
116
+ * One list rather than a field per role, because the roles are exactly what
117
+ * the wires disagree about: a tool result is a message on one and a block
118
+ * inside the previous user turn on another, and what an adapter owes is that
119
+ * the words arrive in the order they were said.
120
+ */
121
+ text: readonly string[];
122
+ /** The names of the tools the request advertised. */
123
+ toolNames: readonly string[];
124
+ /** The JSON Schema the request advertised for each advertised tool. */
125
+ toolSchemas: Readonly<Record<string, JsonValue>>;
126
+ /** Each tool result the request carried, paired with the call it answers. */
127
+ toolResults: readonly {
128
+ callId: string;
129
+ content: string;
130
+ }[];
131
+ /** Each tool call the request carried back, so the provider can pair the result with it. */
132
+ toolCalls: readonly ToolCall[];
133
+ /** Media types of the image and file content the request transmitted, in order. */
134
+ mediaTypes: readonly string[];
135
+ /** The output ceiling the request carried, or absent where it carried none. */
136
+ maxOutputTokens?: number;
137
+ /** The provider-native reasoning control the request carried, verbatim, or absent. */
138
+ effort?: JsonValue;
139
+ /** The sampling control the request carried, or absent. */
140
+ temperature?: number;
141
+ /** How many cache breakpoints the request marked. */
142
+ cacheMarks: number;
143
+ }
144
+ /** A model primed to answer one request, and — where there is a wire — that request. */
145
+ export interface Answering {
146
+ model: Model;
147
+ /** What the one request this model was given actually carried. Wires only. */
148
+ sent?(): SentRequest;
149
+ }
150
+ /**
151
+ * What this implementation does with the three request options a provider may
152
+ * refuse, each of which the port says is honoured or honestly ignored.
153
+ *
154
+ * Declared rather than inferred from the request, because both answers look
155
+ * identical from outside and only one of them is a defect: dropping `temperature`
156
+ * is what the current Anthropic models require, and dropping it silently on a
157
+ * wire that would have accepted it is a caller's control going nowhere.
158
+ */
159
+ export interface Wire {
160
+ /** Whether a level reaches the provider as its own depth control. */
161
+ effort: "sent" | "ignored";
162
+ /** Whether a sampling control reaches a provider that accepts one. */
163
+ temperature: "sent" | "ignored";
164
+ /** Whether `cacheAfter` reaches the provider as a breakpoint. */
165
+ cache: "sent" | "ignored";
166
+ }
167
+ export interface ModelUnderTest {
168
+ /**
169
+ * A model primed to answer one request with `script`.
170
+ *
171
+ * Called once per request, so nothing leaks between cases, and never with a
172
+ * script this subject's declarations say it cannot play.
173
+ */
174
+ answering(script: ModelScript): Answering | Promise<Answering>;
175
+ /**
176
+ * The transport under this implementation, where there is one the subject can
177
+ * script and read back.
178
+ *
179
+ * `"none"` is a claim about the implementation, not a way out of the cases: a
180
+ * model answering from something it already holds has no status to return, no
181
+ * body to cut in half and no request to report — and those are the cases a
182
+ * provider adapter most needs, so one that declares `"none"` is untested where
183
+ * it matters.
184
+ */
185
+ wire: Wire | "none";
186
+ /**
187
+ * Whether a tool call's arguments reach the caller as they arrive.
188
+ *
189
+ * Declared rather than inferred from an empty stream, because "streamed
190
+ * nothing" and "asked for no tools this turn" look the same from outside, and
191
+ * an application rendering a call as it is typed would find out in production.
192
+ */
193
+ toolArguments: "streamed" | "whole";
194
+ /** Whether this implementation can carry a model's reasoning back as deltas. */
195
+ reasoning: "streamed" | "none";
196
+ /**
197
+ * Whether this wire states what a generation cost.
198
+ *
199
+ * A router does — it picks an upstream provider per request and adds its own
200
+ * margin, so what it charged is a thing only it can say, and no rate table an
201
+ * application keeps can reconstruct it. Most wires do not, and there the
202
+ * field stays absent rather than being estimated from one.
203
+ *
204
+ * Declared both ways, because both are a promise: one that reports a cost has
205
+ * to carry it, and one that does not has to leave it alone rather than
206
+ * inventing a zero.
207
+ */
208
+ cost: "reported" | "none";
209
+ }
210
+ export declare function defineModelConformance(subject: ModelUnderTest): readonly ConformanceCase[];