@jxburros/ai-nugget 0.4.1 → 0.7.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.
- package/README.md +158 -382
- package/UPGRADING.md +182 -0
- package/dist/adapters/engines/anthropic.d.ts +7 -1
- package/dist/adapters/engines/anthropic.js +82 -37
- package/dist/adapters/engines/base.d.ts +29 -2
- package/dist/adapters/engines/base.js +52 -8
- package/dist/adapters/engines/google.d.ts +7 -1
- package/dist/adapters/engines/google.js +80 -29
- package/dist/adapters/engines/ollama.d.ts +2 -1
- package/dist/adapters/engines/ollama.js +57 -15
- package/dist/adapters/engines/openaiChat.d.ts +8 -1
- package/dist/adapters/engines/openaiChat.js +109 -29
- package/dist/adapters/profiles.d.ts +21 -1
- package/dist/adapters/profiles.js +12 -2
- package/dist/agent/loop.d.ts +45 -1
- package/dist/agent/loop.js +117 -12
- package/dist/cjs/adapters/engines/anthropic.js +326 -0
- package/dist/cjs/adapters/engines/base.js +147 -0
- package/dist/cjs/adapters/engines/google.js +291 -0
- package/dist/cjs/adapters/engines/ollama.js +205 -0
- package/dist/cjs/adapters/engines/openaiChat.js +312 -0
- package/dist/cjs/adapters/index.js +22 -0
- package/dist/cjs/adapters/profiles.js +125 -0
- package/dist/cjs/agent/index.js +18 -0
- package/dist/cjs/agent/loop.js +373 -0
- package/dist/cjs/agent/tools.js +51 -0
- package/dist/cjs/connect.js +27 -0
- package/dist/cjs/errors.js +110 -0
- package/dist/cjs/handler.js +586 -0
- package/dist/cjs/index.js +28 -0
- package/dist/cjs/json.js +121 -0
- package/dist/cjs/keys.js +81 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/policy.js +42 -0
- package/dist/cjs/reasoning.js +159 -0
- package/dist/cjs/redact.js +78 -0
- package/dist/cjs/tokens.js +22 -0
- package/dist/cjs/transport.js +172 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/util.js +126 -0
- package/dist/errors.js +25 -1
- package/dist/handler.d.ts +67 -1
- package/dist/handler.js +266 -61
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reasoning.d.ts +45 -0
- package/dist/reasoning.js +154 -0
- package/dist/redact.d.ts +18 -0
- package/dist/redact.js +29 -1
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js +21 -1
- package/dist/types.d.ts +112 -2
- package/dist/util.d.ts +22 -0
- package/dist/util.js +59 -0
- package/nugget/VERSION.txt +2 -2
- package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
- package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/anthropic.js +323 -0
- package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
- package/nugget/dist/adapters/engines/base.d.ts +50 -0
- package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/base.js +137 -0
- package/nugget/dist/adapters/engines/base.js.map +1 -0
- package/nugget/dist/adapters/engines/google.d.ts +24 -0
- package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/google.js +288 -0
- package/nugget/dist/adapters/engines/google.js.map +1 -0
- package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
- package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/ollama.js +202 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts +22 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.js +308 -0
- package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
- package/nugget/dist/adapters/index.d.ts +5 -0
- package/nugget/dist/adapters/index.d.ts.map +1 -0
- package/nugget/dist/adapters/index.js +17 -0
- package/nugget/dist/adapters/index.js.map +1 -0
- package/nugget/dist/adapters/profiles.d.ts +62 -0
- package/nugget/dist/adapters/profiles.d.ts.map +1 -0
- package/nugget/dist/adapters/profiles.js +121 -0
- package/nugget/dist/adapters/profiles.js.map +1 -0
- package/nugget/dist/agent/index.d.ts +3 -0
- package/nugget/dist/agent/index.d.ts.map +1 -0
- package/nugget/dist/agent/index.js +3 -0
- package/nugget/dist/agent/index.js.map +1 -0
- package/nugget/dist/agent/loop.d.ts +114 -0
- package/nugget/dist/agent/loop.d.ts.map +1 -0
- package/nugget/dist/agent/loop.js +371 -0
- package/nugget/dist/agent/loop.js.map +1 -0
- package/nugget/dist/agent/tools.d.ts +29 -0
- package/nugget/dist/agent/tools.d.ts.map +1 -0
- package/nugget/dist/agent/tools.js +48 -0
- package/nugget/dist/agent/tools.js.map +1 -0
- package/nugget/dist/cjs/adapters/engines/anthropic.js +326 -0
- package/nugget/dist/cjs/adapters/engines/base.js +147 -0
- package/nugget/dist/cjs/adapters/engines/google.js +291 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +205 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +312 -0
- package/nugget/dist/cjs/adapters/index.js +22 -0
- package/nugget/dist/cjs/adapters/profiles.js +125 -0
- package/nugget/dist/cjs/agent/index.js +18 -0
- package/nugget/dist/cjs/agent/loop.js +373 -0
- package/nugget/dist/cjs/agent/tools.js +51 -0
- package/nugget/dist/cjs/connect.js +27 -0
- package/nugget/dist/cjs/errors.js +110 -0
- package/nugget/dist/cjs/handler.js +586 -0
- package/nugget/dist/cjs/index.js +28 -0
- package/nugget/dist/cjs/json.js +121 -0
- package/nugget/dist/cjs/keys.js +81 -0
- package/nugget/dist/cjs/package.json +3 -0
- package/nugget/dist/cjs/policy.js +42 -0
- package/nugget/dist/cjs/reasoning.js +159 -0
- package/nugget/dist/cjs/redact.js +78 -0
- package/nugget/dist/cjs/tokens.js +22 -0
- package/nugget/dist/cjs/transport.js +172 -0
- package/nugget/dist/cjs/types.js +6 -0
- package/nugget/dist/cjs/util.js +126 -0
- package/nugget/dist/connect.d.ts +31 -0
- package/nugget/dist/connect.d.ts.map +1 -0
- package/nugget/dist/connect.js +25 -0
- package/nugget/dist/connect.js.map +1 -0
- package/nugget/dist/errors.d.ts +22 -0
- package/nugget/dist/errors.d.ts.map +1 -0
- package/nugget/dist/errors.js +104 -0
- package/nugget/dist/errors.js.map +1 -0
- package/nugget/dist/handler.d.ts +114 -0
- package/nugget/dist/handler.d.ts.map +1 -0
- package/nugget/dist/handler.js +582 -0
- package/nugget/dist/handler.js.map +1 -0
- package/nugget/dist/index.d.ts +13 -0
- package/nugget/dist/index.d.ts.map +1 -0
- package/nugget/dist/index.js +13 -0
- package/nugget/dist/index.js.map +1 -0
- package/nugget/dist/json.d.ts +7 -0
- package/nugget/dist/json.d.ts.map +1 -0
- package/nugget/dist/json.js +114 -0
- package/nugget/dist/json.js.map +1 -0
- package/nugget/dist/keys.d.ts +7 -0
- package/nugget/dist/keys.d.ts.map +1 -0
- package/nugget/dist/keys.js +75 -0
- package/nugget/dist/keys.js.map +1 -0
- package/nugget/dist/policy.d.ts +6 -0
- package/nugget/dist/policy.d.ts.map +1 -0
- package/nugget/dist/policy.js +37 -0
- package/nugget/dist/policy.js.map +1 -0
- package/nugget/dist/reasoning.d.ts +45 -0
- package/nugget/dist/reasoning.d.ts.map +1 -0
- package/nugget/dist/reasoning.js +154 -0
- package/nugget/dist/reasoning.js.map +1 -0
- package/nugget/dist/redact.d.ts +26 -0
- package/nugget/dist/redact.d.ts.map +1 -0
- package/nugget/dist/redact.js +74 -0
- package/nugget/dist/redact.js.map +1 -0
- package/nugget/dist/tokens.d.ts +5 -0
- package/nugget/dist/tokens.d.ts.map +1 -0
- package/nugget/dist/tokens.js +18 -0
- package/nugget/dist/tokens.js.map +1 -0
- package/nugget/dist/transport.d.ts +30 -0
- package/nugget/dist/transport.d.ts.map +1 -0
- package/nugget/dist/transport.js +165 -0
- package/nugget/dist/transport.js.map +1 -0
- package/nugget/dist/types.d.ts +301 -0
- package/nugget/dist/types.d.ts.map +1 -0
- package/nugget/dist/types.js +4 -0
- package/nugget/dist/types.js.map +1 -0
- package/nugget/dist/util.d.ts +31 -0
- package/nugget/dist/util.d.ts.map +1 -0
- package/nugget/dist/util.js +114 -0
- package/nugget/dist/util.js.map +1 -0
- package/nugget/src/adapters/engines/anthropic.ts +78 -37
- package/nugget/src/adapters/engines/base.ts +50 -7
- package/nugget/src/adapters/engines/google.ts +79 -29
- package/nugget/src/adapters/engines/ollama.ts +56 -17
- package/nugget/src/adapters/engines/openaiChat.ts +104 -30
- package/nugget/src/adapters/profiles.ts +53 -3
- package/nugget/src/agent/loop.ts +156 -13
- package/nugget/src/errors.ts +25 -1
- package/nugget/src/handler.ts +308 -59
- package/nugget/src/index.ts +1 -0
- package/nugget/src/reasoning.ts +174 -0
- package/nugget/src/redact.ts +32 -1
- package/nugget/src/transport.ts +20 -1
- package/nugget/src/types.ts +113 -1
- package/nugget/src/util.ts +64 -0
- package/package.json +18 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
3
|
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
-
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
5
|
-
import { DEFAULT_TIMEOUT_MS, streamError, streamTimeout } from './base.js';
|
|
4
|
+
import { applyProviderOptions, REASONING_BUDGET_TOKENS, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
5
|
+
import { DEFAULT_TIMEOUT_MS, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
6
6
|
export class GoogleAdapter {
|
|
7
7
|
provider;
|
|
8
8
|
constructor(provider) {
|
|
@@ -30,12 +30,23 @@ export class GoogleAdapter {
|
|
|
30
30
|
const toolCalls = [];
|
|
31
31
|
const timeout = streamTimeout(conn, req.signal);
|
|
32
32
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
33
|
+
// Gemini rejects `responseMimeType: application/json` combined with function
|
|
34
|
+
// declarations, so `body()` drops JSON mode when tools are present. Surface
|
|
35
|
+
// that downgrade instead of silently returning unstructured text.
|
|
36
|
+
if (jsonModeDowngraded(req)) {
|
|
37
|
+
yield {
|
|
38
|
+
type: 'context',
|
|
39
|
+
kind: 'json_mode_downgraded',
|
|
40
|
+
data: { reason: 'Gemini does not accept JSON response mode together with tools; the request was sent without JSON mode', provider: conn.provider },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
33
43
|
try {
|
|
34
44
|
const streamUrl = `${conn.baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:streamGenerateContent?alt=sse`;
|
|
35
45
|
const res = await postResponse(streamUrl, body(req), conn.headers, timeout.signal, conn.provider);
|
|
36
46
|
const contentType = res.headers.get('content-type') ?? '';
|
|
37
47
|
const chunks = contentType.includes('text/event-stream') ? sseLines(res) : singleJsonLine(res);
|
|
38
48
|
for await (const line of chunks) {
|
|
49
|
+
timeout.bump();
|
|
39
50
|
const parsed = safeParse(line);
|
|
40
51
|
const record = asRecord(parsed);
|
|
41
52
|
const promptFeedback = asRecord(record?.promptFeedback);
|
|
@@ -44,6 +55,12 @@ export class GoogleAdapter {
|
|
|
44
55
|
for (const part of candidateParts(record)) {
|
|
45
56
|
const partText = asString(asRecord(part)?.text);
|
|
46
57
|
if (partText) {
|
|
58
|
+
// Gemini marks reasoning parts with `thought: true`; route those to
|
|
59
|
+
// the reasoning channel so they don't blend into the answer text.
|
|
60
|
+
if (asRecord(part)?.thought === true) {
|
|
61
|
+
yield { type: 'reasoning', text: partText };
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
47
64
|
if (firstTokenMs === null)
|
|
48
65
|
firstTokenMs = Date.now() - started;
|
|
49
66
|
text += partText;
|
|
@@ -66,6 +83,8 @@ export class GoogleAdapter {
|
|
|
66
83
|
inputTokens = asNumber(usage?.promptTokenCount) ?? inputTokens;
|
|
67
84
|
outputTokens = asNumber(usage?.candidatesTokenCount) ?? outputTokens;
|
|
68
85
|
}
|
|
86
|
+
if (!finish)
|
|
87
|
+
yield streamAnomaly('stream ended without a finishReason');
|
|
69
88
|
const hasTools = toolCalls.length > 0;
|
|
70
89
|
yield { type: 'done', result: {
|
|
71
90
|
text,
|
|
@@ -92,7 +111,7 @@ export class GoogleAdapter {
|
|
|
92
111
|
*/
|
|
93
112
|
async health(conn) {
|
|
94
113
|
try {
|
|
95
|
-
await fetchJson(
|
|
114
|
+
await fetchJson(joinUrl(conn.baseUrl, '/v1beta/models'), {
|
|
96
115
|
method: 'GET',
|
|
97
116
|
headers: conn.headers,
|
|
98
117
|
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10_000),
|
|
@@ -104,6 +123,41 @@ export class GoogleAdapter {
|
|
|
104
123
|
return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
|
|
105
124
|
}
|
|
106
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Lists models from Google's `/v1beta/models`. Strips the `models/` name
|
|
128
|
+
* prefix, and maps `inputTokenLimit` → `contextWindow` and
|
|
129
|
+
* `supportedGenerationMethods` → `capabilities`.
|
|
130
|
+
*/
|
|
131
|
+
async listModels(conn) {
|
|
132
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/v1beta/models'), {
|
|
133
|
+
method: 'GET',
|
|
134
|
+
headers: conn.headers,
|
|
135
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 15_000),
|
|
136
|
+
provider: conn.provider,
|
|
137
|
+
});
|
|
138
|
+
const rows = Array.isArray(asRecord(data)?.models) ? asRecord(data).models : [];
|
|
139
|
+
const models = [];
|
|
140
|
+
for (const value of rows) {
|
|
141
|
+
const row = asRecord(value);
|
|
142
|
+
const name = asString(row?.name);
|
|
143
|
+
if (!name)
|
|
144
|
+
continue;
|
|
145
|
+
const id = name.startsWith('models/') ? name.slice('models/'.length) : name;
|
|
146
|
+
const model = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
|
|
147
|
+
const contextWindow = asNumber(row?.inputTokenLimit);
|
|
148
|
+
if (contextWindow !== undefined)
|
|
149
|
+
model.contextWindow = contextWindow;
|
|
150
|
+
const methods = row?.supportedGenerationMethods;
|
|
151
|
+
if (Array.isArray(methods) && methods.every((m) => typeof m === 'string'))
|
|
152
|
+
model.capabilities = methods;
|
|
153
|
+
models.push(model);
|
|
154
|
+
}
|
|
155
|
+
return models;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** True when the caller asked for JSON mode but tools force it off. */
|
|
159
|
+
function jsonModeDowngraded(req) {
|
|
160
|
+
return req.responseFormat?.type === 'json' && !!req.tools?.length;
|
|
107
161
|
}
|
|
108
162
|
function body(req) {
|
|
109
163
|
const systemText = req.messages.filter((m) => m.role === 'system').map((m) => textContent(m.content)).join('\n\n');
|
|
@@ -130,7 +184,17 @@ function body(req) {
|
|
|
130
184
|
config.allowedFunctionNames = [req.toolChoice.name];
|
|
131
185
|
payload.toolConfig = { functionCallingConfig: config };
|
|
132
186
|
}
|
|
133
|
-
|
|
187
|
+
if (req.reasoningEffort !== undefined) {
|
|
188
|
+
// First-class reasoning effort → Gemini's thinking budget. 0 disables
|
|
189
|
+
// thinking on models that allow it; others treat it as a minimum.
|
|
190
|
+
payload.generationConfig.thinkingConfig = {
|
|
191
|
+
thinkingBudget: REASONING_BUDGET_TOKENS[req.reasoningEffort],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// providerOptions reaches Google-native fields: top-level `safetySettings`,
|
|
195
|
+
// `cachedContent`, and `generationConfig` extras (`thinkingConfig`,
|
|
196
|
+
// `responseModalities`, …) merged one level deep into generationConfig.
|
|
197
|
+
return applyProviderOptions(payload, req.providerOptions, ['generationConfig']);
|
|
134
198
|
}
|
|
135
199
|
function toGoogleContents(messages) {
|
|
136
200
|
const contents = [];
|
|
@@ -152,29 +216,27 @@ function toGoogleContents(messages) {
|
|
|
152
216
|
return contents;
|
|
153
217
|
}
|
|
154
218
|
function toGoogleContent(m) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
role: 'user',
|
|
158
|
-
parts: [{ functionResponse: { name: m.name ?? 'unknown', response: toolResponseObject(m) } }],
|
|
159
|
-
};
|
|
160
|
-
}
|
|
219
|
+
// Tool-role messages are handled by toGoogleContents' batching loop and never
|
|
220
|
+
// reach here, so no tool branch is needed.
|
|
161
221
|
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
162
|
-
const parts = [];
|
|
163
|
-
if (typeof m.content === 'string' && m.content)
|
|
164
|
-
parts.push({ text: m.content });
|
|
222
|
+
const parts = [...googleParts(m.content)];
|
|
165
223
|
for (const call of m.toolCalls)
|
|
166
224
|
parts.push({ functionCall: { name: call.name, args: call.arguments ?? {} } });
|
|
167
225
|
return { role: 'model', parts };
|
|
168
226
|
}
|
|
169
227
|
return {
|
|
170
228
|
role: m.role === 'assistant' ? 'model' : 'user',
|
|
171
|
-
parts:
|
|
172
|
-
? [{ text: m.content }]
|
|
173
|
-
: m.content.map((part) => part.type === 'image'
|
|
174
|
-
? { inlineData: { mimeType: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
175
|
-
: { text: part.text ?? '' }),
|
|
229
|
+
parts: googleParts(m.content),
|
|
176
230
|
};
|
|
177
231
|
}
|
|
232
|
+
/** Maps text/image content — a string or a ContentPart[] — onto Google parts. */
|
|
233
|
+
function googleParts(content) {
|
|
234
|
+
if (typeof content === 'string')
|
|
235
|
+
return content ? [{ text: content }] : [];
|
|
236
|
+
return content.map((part) => part.type === 'image'
|
|
237
|
+
? { inlineData: { mimeType: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
238
|
+
: { text: part.text ?? '' });
|
|
239
|
+
}
|
|
178
240
|
function textContent(content) {
|
|
179
241
|
if (typeof content === 'string')
|
|
180
242
|
return content;
|
|
@@ -223,15 +285,4 @@ function mapFinish(finish, hasTools) {
|
|
|
223
285
|
return 'content_filter';
|
|
224
286
|
return 'stop';
|
|
225
287
|
}
|
|
226
|
-
function safeParse(line) {
|
|
227
|
-
try {
|
|
228
|
-
return JSON.parse(line);
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
function randomId() {
|
|
235
|
-
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
236
|
-
}
|
|
237
288
|
//# sourceMappingURL=google.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
1
|
+
import type { ChatRequest, ChatResult, EmbedRequest, EmbedResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
2
|
export declare class OllamaAdapter implements ProviderAdapter {
|
|
3
3
|
readonly provider: string;
|
|
4
4
|
constructor(provider: string);
|
|
5
5
|
chat(conn: ResolvedConnection, req: ChatRequest): Promise<ChatResult>;
|
|
6
6
|
stream(conn: ResolvedConnection, req: ChatRequest): AsyncIterable<StreamEvent>;
|
|
7
7
|
listModels(conn: ResolvedConnection): Promise<ModelInfo[]>;
|
|
8
|
+
embed(conn: ResolvedConnection, req: EmbedRequest): Promise<EmbedResult>;
|
|
8
9
|
private showModel;
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=ollama.d.ts.map
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
3
|
import { fetchJson, ndjsonLines, postResponse } from '../../transport.js';
|
|
4
|
-
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
5
|
-
import { DEFAULT_TIMEOUT_MS, streamError, streamTimeout } from './base.js';
|
|
4
|
+
import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, mapWithConcurrency, textFromMessages } from '../../util.js';
|
|
5
|
+
import { DEFAULT_TIMEOUT_MS, parseArgs, randomId, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
6
|
+
/** Max concurrent /api/show probes when listing models — enough to be fast, few enough to be polite to a local daemon. */
|
|
7
|
+
const SHOW_CONCURRENCY = 6;
|
|
6
8
|
export class OllamaAdapter {
|
|
7
9
|
provider;
|
|
8
10
|
constructor(provider) {
|
|
@@ -27,14 +29,19 @@ export class OllamaAdapter {
|
|
|
27
29
|
let inputTokens;
|
|
28
30
|
let outputTokens;
|
|
29
31
|
let doneReason;
|
|
32
|
+
let sawTerminal = false;
|
|
30
33
|
const toolCalls = [];
|
|
31
34
|
const timeout = streamTimeout(conn, req.signal);
|
|
32
35
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
33
36
|
try {
|
|
34
|
-
const res = await postResponse(
|
|
37
|
+
const res = await postResponse(joinUrl(conn.baseUrl, '/api/chat'), body(req), conn.headers, timeout.signal, conn.provider);
|
|
35
38
|
for await (const value of ndjsonLines(res)) {
|
|
39
|
+
timeout.bump();
|
|
36
40
|
const record = asRecord(value);
|
|
37
41
|
const message = asRecord(record?.message);
|
|
42
|
+
const thinking = asString(message?.thinking);
|
|
43
|
+
if (thinking)
|
|
44
|
+
yield { type: 'reasoning', text: thinking };
|
|
38
45
|
const piece = asString(message?.content);
|
|
39
46
|
if (piece) {
|
|
40
47
|
if (firstTokenMs === null)
|
|
@@ -45,6 +52,8 @@ export class OllamaAdapter {
|
|
|
45
52
|
inputTokens = asNumber(record?.prompt_eval_count) ?? inputTokens;
|
|
46
53
|
outputTokens = asNumber(record?.eval_count) ?? outputTokens;
|
|
47
54
|
doneReason = asString(record?.done_reason) ?? doneReason;
|
|
55
|
+
if (record?.done === true)
|
|
56
|
+
sawTerminal = true;
|
|
48
57
|
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
49
58
|
for (const callValue of calls) {
|
|
50
59
|
const fn = asRecord(asRecord(callValue)?.function);
|
|
@@ -52,13 +61,18 @@ export class OllamaAdapter {
|
|
|
52
61
|
const toolCall = {
|
|
53
62
|
id: randomId(),
|
|
54
63
|
name: asString(fn?.name) ?? 'unknown',
|
|
55
|
-
|
|
64
|
+
// Ollama itself sends a native object, but llama.cpp-style backends
|
|
65
|
+
// behind the same protocol send a JSON *string* — coerce either into
|
|
66
|
+
// the object `validateToolArgs` expects.
|
|
67
|
+
arguments: parseArgs(args),
|
|
56
68
|
raw: typeof args === 'string' ? args : JSON.stringify(args),
|
|
57
69
|
};
|
|
58
70
|
toolCalls.push(toolCall);
|
|
59
71
|
yield { type: 'tool_call', call: toolCall };
|
|
60
72
|
}
|
|
61
73
|
}
|
|
74
|
+
if (!sawTerminal)
|
|
75
|
+
yield streamAnomaly('NDJSON stream ended without a done record');
|
|
62
76
|
const hasTools = toolCalls.length > 0;
|
|
63
77
|
yield { type: 'done', result: {
|
|
64
78
|
text,
|
|
@@ -86,19 +100,39 @@ export class OllamaAdapter {
|
|
|
86
100
|
});
|
|
87
101
|
const models = Array.isArray(asRecord(data)?.models) ? asRecord(data).models : [];
|
|
88
102
|
const ids = models.map((model) => asString(asRecord(model)?.name) ?? asString(asRecord(model)?.model) ?? '').filter(Boolean);
|
|
89
|
-
// Probe /api/show per model for context window + capabilities (best effort,
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
// Probe /api/show per model for context window + capabilities (best effort),
|
|
104
|
+
// with bounded concurrency so a large model list resolves quickly without
|
|
105
|
+
// opening an unbounded number of sockets against a local daemon.
|
|
106
|
+
return mapWithConcurrency(ids, SHOW_CONCURRENCY, async (id) => {
|
|
93
107
|
const info = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
|
|
94
108
|
const probed = await this.showModel(conn, id).catch(() => undefined);
|
|
95
109
|
if (probed?.contextWindow !== undefined)
|
|
96
110
|
info.contextWindow = probed.contextWindow;
|
|
97
111
|
if (probed?.capabilities)
|
|
98
112
|
info.capabilities = probed.capabilities;
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
113
|
+
return info;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async embed(conn, req) {
|
|
117
|
+
const inputs = Array.isArray(req.input) ? req.input : [req.input];
|
|
118
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/api/embed'), {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'content-type': 'application/json', ...conn.headers },
|
|
121
|
+
body: JSON.stringify(applyProviderOptions({ model: req.model, input: inputs }, req.providerOptions)),
|
|
122
|
+
timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
123
|
+
provider: conn.provider,
|
|
124
|
+
});
|
|
125
|
+
const record = asRecord(data);
|
|
126
|
+
const rows = Array.isArray(record?.embeddings) ? record.embeddings : [];
|
|
127
|
+
const embeddings = rows.map((row) => (Array.isArray(row) ? row.filter((n) => typeof n === 'number') : []));
|
|
128
|
+
const inputTokens = asNumber(record?.prompt_eval_count);
|
|
129
|
+
return {
|
|
130
|
+
embeddings,
|
|
131
|
+
model: req.model,
|
|
132
|
+
usage: { inputTokens, outputTokens: 0, estimated: inputTokens === undefined },
|
|
133
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
134
|
+
raw: data,
|
|
135
|
+
};
|
|
102
136
|
}
|
|
103
137
|
async showModel(conn, model) {
|
|
104
138
|
const { data } = await fetchJson(`${conn.baseUrl}/api/show`, {
|
|
@@ -130,7 +164,7 @@ export class OllamaAdapter {
|
|
|
130
164
|
}
|
|
131
165
|
}
|
|
132
166
|
function body(req) {
|
|
133
|
-
|
|
167
|
+
const base = {
|
|
134
168
|
model: req.model,
|
|
135
169
|
messages: req.messages.map(toOllamaMessage),
|
|
136
170
|
stream: true,
|
|
@@ -138,6 +172,17 @@ function body(req) {
|
|
|
138
172
|
format: req.responseFormat?.type === 'json' ? (req.responseFormat.schema ?? 'json') : undefined,
|
|
139
173
|
tools: req.tools?.map((tool) => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })),
|
|
140
174
|
};
|
|
175
|
+
// First-class reasoning effort → Ollama's `think` flag (reasoning arrives in
|
|
176
|
+
// `message.thinking`, which this engine already routes to `reasoning` events).
|
|
177
|
+
// Ollama has no graded effort for most models, so anything but `'none'` is
|
|
178
|
+
// `true`. Only sent when asked for: a model without thinking support rejects it.
|
|
179
|
+
if (req.reasoningEffort !== undefined)
|
|
180
|
+
base.think = req.reasoningEffort !== 'none';
|
|
181
|
+
// providerOptions reaches Ollama's real request fields — `options.num_ctx`,
|
|
182
|
+
// `options.num_keep`, top-level `keep_alive`, `think`, etc. `options` is
|
|
183
|
+
// merged one level deep so num_ctx joins the samplers above rather than
|
|
184
|
+
// replacing them.
|
|
185
|
+
return applyProviderOptions(base, req.providerOptions, ['options']);
|
|
141
186
|
}
|
|
142
187
|
function toOllamaMessage(m) {
|
|
143
188
|
const images = typeof m.content === 'string' ? undefined : m.content.filter((part) => part.type === 'image').map((part) => part.imageBase64).filter(Boolean);
|
|
@@ -154,7 +199,4 @@ function toOllamaMessage(m) {
|
|
|
154
199
|
}
|
|
155
200
|
return message;
|
|
156
201
|
}
|
|
157
|
-
function randomId() {
|
|
158
|
-
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
159
|
-
}
|
|
160
202
|
//# sourceMappingURL=ollama.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
1
|
+
import type { ChatRequest, ChatResult, EmbedRequest, EmbedResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
2
|
import type { ProviderProfile } from '../profiles.js';
|
|
3
3
|
export declare class OpenAIChatAdapter implements ProviderAdapter {
|
|
4
4
|
private profile;
|
|
@@ -11,5 +11,12 @@ export declare class OpenAIChatAdapter implements ProviderAdapter {
|
|
|
11
11
|
ok: boolean;
|
|
12
12
|
detail?: string;
|
|
13
13
|
}>;
|
|
14
|
+
embed(conn: ResolvedConnection, req: EmbedRequest): Promise<EmbedResult>;
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* True when a `/chat/completions` 400 is the "function tools with reasoning_effort
|
|
18
|
+
* are not supported" refusal AND the request actually carried tools AND the caller
|
|
19
|
+
* did not already pin `reasoning_effort` (in which case the choice is theirs).
|
|
20
|
+
*/
|
|
21
|
+
export declare function shouldRetryWithoutReasoningEffort(error: unknown, req: ChatRequest): boolean;
|
|
15
22
|
//# sourceMappingURL=openaiChat.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
|
-
import { postResponse, sseLines } from '../../transport.js';
|
|
4
|
-
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
5
|
-
import { health, listOpenModels, streamError, streamTimeout } from './base.js';
|
|
3
|
+
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
+
import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
5
|
+
import { DEFAULT_TIMEOUT_MS, health, listOpenModels, parseArgs, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
6
6
|
export class OpenAIChatAdapter {
|
|
7
7
|
profile;
|
|
8
8
|
provider;
|
|
@@ -34,7 +34,34 @@ export class OpenAIChatAdapter {
|
|
|
34
34
|
const timeout = streamTimeout(conn, req.signal);
|
|
35
35
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
36
36
|
try {
|
|
37
|
-
|
|
37
|
+
let res;
|
|
38
|
+
try {
|
|
39
|
+
res = await postResponse(urlFor(conn, this.profile, req), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// OpenAI's `/chat/completions` refuses function tools on a reasoning model
|
|
43
|
+
// unless `reasoning_effort` is `'none'` ("Function tools with reasoning_effort
|
|
44
|
+
// are not supported … use /v1/responses or set reasoning_effort to 'none'").
|
|
45
|
+
// The nugget never sets `reasoning_effort` itself, so the provider default
|
|
46
|
+
// applies and every tool-using turn 400s. Rather than make each app learn
|
|
47
|
+
// this the hard way, retry the one request once with the effort disabled
|
|
48
|
+
// and say so on the stream. Reactive on purpose: sending
|
|
49
|
+
// `reasoning_effort` up front would 400 on non-reasoning models instead.
|
|
50
|
+
// A caller-requested effort is overridden too — the alternative is a hard
|
|
51
|
+
// 400 with no tool call at all — and the context event says so.
|
|
52
|
+
if (!shouldRetryWithoutReasoningEffort(error, req))
|
|
53
|
+
throw error;
|
|
54
|
+
yield {
|
|
55
|
+
type: 'context',
|
|
56
|
+
kind: 'reasoning_effort_disabled_for_tools',
|
|
57
|
+
data: {
|
|
58
|
+
reason: 'provider rejected function tools with reasoning_effort; retried with reasoning_effort: none',
|
|
59
|
+
requested: req.providerOptions?.reasoning_effort ?? req.reasoningEffort ?? null,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const retryReq = { ...req, providerOptions: { ...req.providerOptions, reasoning_effort: 'none' } };
|
|
63
|
+
res = await postResponse(urlFor(conn, this.profile, retryReq), openAiBody(retryReq, this.profile), conn.headers, timeout.signal, conn.provider);
|
|
64
|
+
}
|
|
38
65
|
const contentType = res.headers.get('content-type') ?? '';
|
|
39
66
|
if (!contentType.includes('text/event-stream')) {
|
|
40
67
|
// Server ignored stream:true (or is a buffered gateway) — recover the whole body.
|
|
@@ -47,6 +74,8 @@ export class OpenAIChatAdapter {
|
|
|
47
74
|
outputTokens = parsed.outputTokens;
|
|
48
75
|
finish = parsed.finish;
|
|
49
76
|
sawTerminal = true;
|
|
77
|
+
if (parsed.reasoning)
|
|
78
|
+
yield { type: 'reasoning', text: parsed.reasoning };
|
|
50
79
|
if (text)
|
|
51
80
|
yield { type: 'delta', text };
|
|
52
81
|
for (const call of toolCalls)
|
|
@@ -55,6 +84,7 @@ export class OpenAIChatAdapter {
|
|
|
55
84
|
else {
|
|
56
85
|
const partialTools = new Map();
|
|
57
86
|
for await (const line of sseLines(res)) {
|
|
87
|
+
timeout.bump();
|
|
58
88
|
const chunk = safeParse(line);
|
|
59
89
|
const record = asRecord(chunk);
|
|
60
90
|
if (!record)
|
|
@@ -68,6 +98,11 @@ export class OpenAIChatAdapter {
|
|
|
68
98
|
if (choice?.finish_reason)
|
|
69
99
|
sawTerminal = true;
|
|
70
100
|
const delta = asRecord(choice?.delta);
|
|
101
|
+
// Reasoning tokens (DeepSeek `reasoning_content`, others `reasoning`)
|
|
102
|
+
// ride their own channel so they don't contaminate the answer text.
|
|
103
|
+
const reasoning = asString(delta?.reasoning_content) ?? asString(delta?.reasoning);
|
|
104
|
+
if (reasoning)
|
|
105
|
+
yield { type: 'reasoning', text: reasoning };
|
|
71
106
|
const piece = asString(delta?.content);
|
|
72
107
|
if (piece) {
|
|
73
108
|
if (firstTokenMs === null)
|
|
@@ -96,9 +131,8 @@ export class OpenAIChatAdapter {
|
|
|
96
131
|
for (const call of toolCalls)
|
|
97
132
|
yield { type: 'tool_call', call };
|
|
98
133
|
}
|
|
99
|
-
if (!sawTerminal)
|
|
100
|
-
yield
|
|
101
|
-
}
|
|
134
|
+
if (!sawTerminal)
|
|
135
|
+
yield streamAnomaly('stream ended without a finish_reason');
|
|
102
136
|
const result = makeResult(conn, req, text, toolCalls, finish, started, firstTokenMs, inputTokens, outputTokens);
|
|
103
137
|
yield { type: 'done', result };
|
|
104
138
|
}
|
|
@@ -115,6 +149,34 @@ export class OpenAIChatAdapter {
|
|
|
115
149
|
health(conn) {
|
|
116
150
|
return health(conn, this.profile);
|
|
117
151
|
}
|
|
152
|
+
async embed(conn, req) {
|
|
153
|
+
const inputs = Array.isArray(req.input) ? req.input : [req.input];
|
|
154
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/embeddings'), {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'content-type': 'application/json', ...conn.headers },
|
|
157
|
+
body: JSON.stringify(applyProviderOptions({ model: req.model, input: inputs }, req.providerOptions)),
|
|
158
|
+
timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
159
|
+
provider: conn.provider,
|
|
160
|
+
});
|
|
161
|
+
const record = asRecord(data);
|
|
162
|
+
const rows = Array.isArray(record?.data) ? record.data : [];
|
|
163
|
+
// Order by the `index` each row reports, falling back to array order.
|
|
164
|
+
const byIndex = rows
|
|
165
|
+
.map((row, i) => ({ index: asNumber(asRecord(row)?.index) ?? i, embedding: asEmbedding(asRecord(row)?.embedding) }))
|
|
166
|
+
.sort((a, b) => a.index - b.index);
|
|
167
|
+
const usage = asRecord(record?.usage);
|
|
168
|
+
const inputTokens = asNumber(usage?.prompt_tokens);
|
|
169
|
+
return {
|
|
170
|
+
embeddings: byIndex.map((row) => row.embedding),
|
|
171
|
+
model: req.model,
|
|
172
|
+
usage: { inputTokens, outputTokens: 0, estimated: inputTokens === undefined },
|
|
173
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
174
|
+
raw: data,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function asEmbedding(value) {
|
|
179
|
+
return Array.isArray(value) ? value.filter((n) => typeof n === 'number') : [];
|
|
118
180
|
}
|
|
119
181
|
function openAiBody(req, profile) {
|
|
120
182
|
const body = {
|
|
@@ -134,7 +196,16 @@ function openAiBody(req, profile) {
|
|
|
134
196
|
};
|
|
135
197
|
if (req.maxTokens !== undefined)
|
|
136
198
|
body[profile.quirks?.maxTokensParam ?? 'max_tokens'] = req.maxTokens;
|
|
137
|
-
|
|
199
|
+
// First-class reasoning effort → OpenAI's `reasoning_effort`. Only sent when
|
|
200
|
+
// asked for: non-reasoning models reject the parameter outright.
|
|
201
|
+
if (req.reasoningEffort !== undefined)
|
|
202
|
+
body.reasoning_effort = req.reasoningEffort;
|
|
203
|
+
// providerOptions carries OpenAI-native fields the nugget doesn't model
|
|
204
|
+
// (`reasoning_effort`, `parallel_tool_calls`, `seed`, `logprobs`, …). `apiVersion`
|
|
205
|
+
// is consumed by urlFor for Azure and stripped here so it never hits the body.
|
|
206
|
+
const { apiVersion, ...passthrough } = req.providerOptions ?? {};
|
|
207
|
+
void apiVersion;
|
|
208
|
+
return applyProviderOptions(body, Object.keys(passthrough).length ? passthrough : undefined);
|
|
138
209
|
}
|
|
139
210
|
function responseFormatFor(req, profile) {
|
|
140
211
|
if (req.responseFormat?.type !== 'json')
|
|
@@ -157,11 +228,18 @@ function toOpenAiMessage(message) {
|
|
|
157
228
|
tool_calls: message.toolCalls?.map((call) => ({ id: call.id, type: 'function', function: { name: call.name, arguments: call.raw ?? JSON.stringify(call.arguments) } })),
|
|
158
229
|
};
|
|
159
230
|
}
|
|
160
|
-
function urlFor(conn, profile,
|
|
231
|
+
function urlFor(conn, profile, req) {
|
|
161
232
|
if (profile.quirks?.urlTemplate) {
|
|
162
|
-
|
|
233
|
+
// `{apiVersion}` (Azure) is filled from a per-call override, then the
|
|
234
|
+
// profile default — so a retired Azure api-version is fixable without a
|
|
235
|
+
// library release: pass `providerOptions: { apiVersion: '2025-01-01-preview' }`.
|
|
236
|
+
const apiVersion = asString(req.providerOptions?.apiVersion) ?? profile.quirks.apiVersion ?? '';
|
|
237
|
+
return profile.quirks.urlTemplate
|
|
238
|
+
.replace('{baseUrl}', conn.baseUrl)
|
|
239
|
+
.replace('{model}', encodeURIComponent(req.model))
|
|
240
|
+
.replace('{apiVersion}', apiVersion);
|
|
163
241
|
}
|
|
164
|
-
return
|
|
242
|
+
return joinUrl(conn.baseUrl, '/chat/completions');
|
|
165
243
|
}
|
|
166
244
|
function parseOpenAiResponse(raw) {
|
|
167
245
|
const record = asRecord(raw);
|
|
@@ -169,9 +247,11 @@ function parseOpenAiResponse(raw) {
|
|
|
169
247
|
const choice = asRecord(Array.isArray(record?.choices) ? record.choices[0] : undefined);
|
|
170
248
|
const message = asRecord(choice?.message);
|
|
171
249
|
const text = asString(message?.content) ?? asString(record?.text) ?? '';
|
|
250
|
+
const reasoning = asString(message?.reasoning_content) ?? asString(message?.reasoning);
|
|
172
251
|
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
173
252
|
return {
|
|
174
253
|
text,
|
|
254
|
+
reasoning,
|
|
175
255
|
finish: asString(choice?.finish_reason),
|
|
176
256
|
toolCalls: calls.map((value) => {
|
|
177
257
|
const call = asRecord(value);
|
|
@@ -206,23 +286,23 @@ function mapFinish(finish, hasToolCalls) {
|
|
|
206
286
|
return 'content_filter';
|
|
207
287
|
return 'stop';
|
|
208
288
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return
|
|
289
|
+
/**
|
|
290
|
+
* True when a `/chat/completions` 400 is the "function tools with reasoning_effort
|
|
291
|
+
* are not supported" refusal AND the request actually carried tools AND the caller
|
|
292
|
+
* did not already pin `reasoning_effort` (in which case the choice is theirs).
|
|
293
|
+
*/
|
|
294
|
+
export function shouldRetryWithoutReasoningEffort(error, req) {
|
|
295
|
+
if (!(error instanceof AIError) || error.status !== 400)
|
|
296
|
+
return false;
|
|
297
|
+
if (!req.tools || req.tools.length === 0)
|
|
298
|
+
return false;
|
|
299
|
+
// Already `'none'` (from either knob) — the refusal is about something else.
|
|
300
|
+
const effective = req.providerOptions && 'reasoning_effort' in req.providerOptions
|
|
301
|
+
? req.providerOptions.reasoning_effort
|
|
302
|
+
: req.reasoningEffort;
|
|
303
|
+
if (effective === 'none')
|
|
304
|
+
return false;
|
|
305
|
+
const text = `${error.raw ?? ''} ${error.message}`.toLowerCase();
|
|
306
|
+
return text.includes('reasoning_effort') && text.includes('tool');
|
|
227
307
|
}
|
|
228
308
|
//# sourceMappingURL=openaiChat.js.map
|
|
@@ -32,11 +32,31 @@ export interface ProviderProfile {
|
|
|
32
32
|
maxTokensRequired?: boolean;
|
|
33
33
|
maxTokensParam?: 'max_tokens' | 'max_completion_tokens';
|
|
34
34
|
supportsJsonSchema?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Default value for the `{apiVersion}` token in `urlTemplate` (Azure OpenAI).
|
|
37
|
+
* Overridable per call via `ChatRequest.providerOptions.apiVersion`, so a
|
|
38
|
+
* retired Azure api-version never requires a library release.
|
|
39
|
+
*/
|
|
40
|
+
apiVersion?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Provider honors an `Idempotency-Key` request header. When set, the handler
|
|
43
|
+
* attaches one key per logical call and reuses it across retries, so a retry
|
|
44
|
+
* after a connection drops post-generation is deduped by the provider rather
|
|
45
|
+
* than billed twice. Left off unless the provider documents the header —
|
|
46
|
+
* sending an unknown header is harmless, but claiming the guarantee is not.
|
|
47
|
+
*/
|
|
48
|
+
supportsIdempotencyKey?: boolean;
|
|
35
49
|
};
|
|
36
50
|
listModelsPath?: string;
|
|
37
51
|
healthPath?: string;
|
|
38
52
|
}
|
|
39
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The provider keys that have a profile in {@link PROVIDER_PROFILES}. Used to
|
|
55
|
+
* type `Connection.provider` so IDEs autocomplete valid names and typos are a
|
|
56
|
+
* compile error rather than a silent fall-through to `openai-compat`.
|
|
57
|
+
*/
|
|
58
|
+
export type KnownProvider = 'openai' | 'azure-openai' | 'openrouter' | 'groq' | 'deepseek' | 'mistral' | 'together' | 'fireworks' | 'cerebras' | 'moonshot' | 'cohere' | 'perplexity' | 'lmstudio' | 'llamacpp' | 'vllm' | 'ollama' | 'anthropic' | 'google' | 'openai-compat';
|
|
59
|
+
export declare const PROVIDER_PROFILES: Record<KnownProvider, ProviderProfile> & Record<string, ProviderProfile>;
|
|
40
60
|
export declare function profileFor(provider: string, baseUrl?: string): ProviderProfile;
|
|
41
61
|
export declare function applyAuth(profile: ProviderProfile, apiKey: string | null, headers: Record<string, string>): Record<string, string>;
|
|
42
62
|
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -7,13 +7,13 @@ export const PROVIDER_PROFILES = {
|
|
|
7
7
|
auth: 'bearer',
|
|
8
8
|
listModelsPath: '/models',
|
|
9
9
|
capabilities: HOSTED_CLOUD,
|
|
10
|
-
quirks: { supportsUsageInStream: true, maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true },
|
|
10
|
+
quirks: { supportsUsageInStream: true, maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true, supportsIdempotencyKey: true },
|
|
11
11
|
},
|
|
12
12
|
'azure-openai': {
|
|
13
13
|
engine: 'openaiChat',
|
|
14
14
|
auth: 'api-key-header',
|
|
15
15
|
capabilities: HOSTED_CLOUD,
|
|
16
|
-
quirks: { urlTemplate: '{baseUrl}/openai/deployments/{model}/chat/completions?api-version=2024-10-21', maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true },
|
|
16
|
+
quirks: { urlTemplate: '{baseUrl}/openai/deployments/{model}/chat/completions?api-version={apiVersion}', apiVersion: '2024-10-21', maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true },
|
|
17
17
|
},
|
|
18
18
|
openrouter: {
|
|
19
19
|
engine: 'openaiChat',
|
|
@@ -29,6 +29,12 @@ export const PROVIDER_PROFILES = {
|
|
|
29
29
|
mistral: { engine: 'openaiChat', defaultBaseUrl: 'https://api.mistral.ai/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
30
30
|
together: { engine: 'openaiChat', defaultBaseUrl: 'https://api.together.xyz/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
31
31
|
fireworks: { engine: 'openaiChat', defaultBaseUrl: 'https://api.fireworks.ai/inference/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
32
|
+
cerebras: { engine: 'openaiChat', defaultBaseUrl: 'https://api.cerebras.ai/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
33
|
+
moonshot: { engine: 'openaiChat', defaultBaseUrl: 'https://api.moonshot.ai/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
34
|
+
// Cohere's OpenAI-compatible surface lives under /compatibility/v1.
|
|
35
|
+
cohere: { engine: 'openaiChat', defaultBaseUrl: 'https://api.cohere.ai/compatibility/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
36
|
+
// Perplexity has no public model-listing endpoint, so listModels resolves empty.
|
|
37
|
+
perplexity: { engine: 'openaiChat', defaultBaseUrl: 'https://api.perplexity.ai', auth: 'bearer', capabilities: HOSTED_CLOUD },
|
|
32
38
|
lmstudio: {
|
|
33
39
|
engine: 'openaiChat',
|
|
34
40
|
defaultBaseUrl: 'http://localhost:1234/v1',
|
|
@@ -86,6 +92,10 @@ export const PROVIDER_PROFILES = {
|
|
|
86
92
|
// Unknown endpoint — usually pointed at a local/self-hosted server, so
|
|
87
93
|
// assume the conservative local-runtime defaults rather than the hosted-cloud ones.
|
|
88
94
|
capabilities: LOCAL_RUNTIME,
|
|
95
|
+
// Most servers reached through this escape hatch (JX Runtime and other
|
|
96
|
+
// local OpenAI-compatible runtimes) don't require a key at all; keep
|
|
97
|
+
// `auth: 'bearer'` for the ones that do (a key is only sent if provided).
|
|
98
|
+
quirks: { keyOptional: true },
|
|
89
99
|
},
|
|
90
100
|
};
|
|
91
101
|
export function profileFor(provider, baseUrl) {
|