@jxburros/ai-nugget 0.3.1 → 0.6.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 +134 -271
- package/UPGRADING.md +158 -0
- package/dist/adapters/engines/anthropic.d.ts +7 -1
- package/dist/adapters/engines/anthropic.js +64 -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 +17 -1
- package/dist/adapters/engines/google.js +93 -29
- package/dist/adapters/engines/ollama.d.ts +2 -1
- package/dist/adapters/engines/ollama.js +51 -15
- package/dist/adapters/engines/openaiChat.d.ts +2 -1
- package/dist/adapters/engines/openaiChat.js +60 -30
- package/dist/adapters/profiles.d.ts +21 -1
- package/dist/adapters/profiles.js +12 -2
- package/dist/agent/loop.d.ts +43 -1
- package/dist/agent/loop.js +120 -13
- package/dist/cjs/adapters/engines/anthropic.js +308 -0
- package/dist/cjs/adapters/engines/base.js +147 -0
- package/dist/cjs/adapters/engines/google.js +284 -0
- package/dist/cjs/adapters/engines/ollama.js +199 -0
- package/dist/cjs/adapters/engines/openaiChat.js +261 -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 +372 -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 +561 -0
- package/dist/cjs/index.js +27 -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/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 +112 -0
- package/dist/connect.d.ts +31 -0
- package/dist/connect.js +25 -0
- package/dist/errors.js +25 -1
- package/dist/handler.d.ts +61 -1
- package/dist/handler.js +247 -61
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keys.js +1 -4
- 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 +96 -2
- package/dist/util.d.ts +16 -0
- package/dist/util.js +50 -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 +305 -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 +281 -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 +196 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts +16 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.js +258 -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 +112 -0
- package/nugget/dist/agent/loop.d.ts.map +1 -0
- package/nugget/dist/agent/loop.js +370 -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 +308 -0
- package/nugget/dist/cjs/adapters/engines/base.js +147 -0
- package/nugget/dist/cjs/adapters/engines/google.js +284 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +199 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +261 -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 +372 -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 +561 -0
- package/nugget/dist/cjs/index.js +27 -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/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 +112 -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 +108 -0
- package/nugget/dist/handler.d.ts.map +1 -0
- package/nugget/dist/handler.js +557 -0
- package/nugget/dist/handler.js.map +1 -0
- package/nugget/dist/index.d.ts +12 -0
- package/nugget/dist/index.d.ts.map +1 -0
- package/nugget/dist/index.js +12 -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/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 +285 -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 +24 -0
- package/nugget/dist/util.d.ts.map +1 -0
- package/nugget/dist/util.js +101 -0
- package/nugget/dist/util.js.map +1 -0
- package/nugget/src/adapters/engines/anthropic.ts +62 -37
- package/nugget/src/adapters/engines/base.ts +50 -7
- package/nugget/src/adapters/engines/google.ts +92 -29
- package/nugget/src/adapters/engines/ollama.ts +51 -17
- package/nugget/src/adapters/engines/openaiChat.ts +61 -32
- package/nugget/src/adapters/profiles.ts +53 -3
- package/nugget/src/agent/loop.ts +158 -14
- package/nugget/src/connect.ts +48 -0
- package/nugget/src/errors.ts +25 -1
- package/nugget/src/handler.ts +286 -59
- package/nugget/src/index.ts +1 -0
- package/nugget/src/keys.ts +1 -5
- package/nugget/src/redact.ts +32 -1
- package/nugget/src/transport.ts +20 -1
- package/nugget/src/types.ts +96 -1
- package/nugget/src/util.ts +55 -0
- package/package.json +20 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AIError, fromUnknown } from '../../errors.js';
|
|
2
2
|
import { fetchJson, withTimeout } from '../../transport.js';
|
|
3
3
|
import type { ModelInfo, ResolvedConnection } from '../../types.js';
|
|
4
|
-
import { asRecord, asString } from '../../util.js';
|
|
4
|
+
import { asRecord, asString, joinUrl } from '../../util.js';
|
|
5
5
|
import type { ProviderProfile } from '../profiles.js';
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
@@ -17,14 +17,18 @@ export function streamError(error: unknown, timeout: { timedOut(): boolean }, pr
|
|
|
17
17
|
return fromUnknown(error, provider);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Create a timeout/abort scope covering the full stream lifetime, plus an
|
|
22
|
+
* optional idle timeout (`conn.idleTimeoutMs`) that the engine rearms via
|
|
23
|
+
* `bump()` on each received chunk.
|
|
24
|
+
*/
|
|
21
25
|
export function streamTimeout(conn: ResolvedConnection, signal?: AbortSignal): ReturnType<typeof withTimeout> {
|
|
22
|
-
return withTimeout(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal);
|
|
26
|
+
return withTimeout(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal, conn.idleTimeoutMs);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export async function listOpenModels(conn: ResolvedConnection, profile: ProviderProfile): Promise<ModelInfo[]> {
|
|
26
30
|
if (!profile.listModelsPath) return [];
|
|
27
|
-
const { data } = await fetchJson(
|
|
31
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, profile.listModelsPath), {
|
|
28
32
|
method: 'GET',
|
|
29
33
|
headers: conn.headers,
|
|
30
34
|
timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
@@ -54,7 +58,7 @@ export async function health(conn: ResolvedConnection, profile: ProviderProfile)
|
|
|
54
58
|
const path = profile.healthPath ?? profile.listModelsPath;
|
|
55
59
|
if (!path) return { ok: true };
|
|
56
60
|
try {
|
|
57
|
-
await fetchJson(
|
|
61
|
+
await fetchJson(joinUrl(conn.baseUrl, path), {
|
|
58
62
|
method: 'GET',
|
|
59
63
|
headers: conn.headers,
|
|
60
64
|
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10_000),
|
|
@@ -66,8 +70,47 @@ export async function health(conn: ResolvedConnection, profile: ProviderProfile)
|
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Coerce a provider's tool-call `arguments` into an object. Providers disagree
|
|
75
|
+
* on the shape: OpenAI/Anthropic stream a JSON *string*, Google and Ollama send
|
|
76
|
+
* a native object — but Ollama-compatible backends (llama.cpp and friends)
|
|
77
|
+
* sometimes send a string too. Malformed JSON degrades to `{}` so a single bad
|
|
78
|
+
* tool call surfaces as an argument-validation error rather than a stream crash.
|
|
79
|
+
*/
|
|
80
|
+
export function parseArgs(raw: unknown): unknown {
|
|
81
|
+
if (raw === undefined || raw === null) return {};
|
|
82
|
+
if (typeof raw !== 'string') return raw;
|
|
83
|
+
if (!raw.trim()) return {};
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(raw) as unknown;
|
|
86
|
+
} catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** JSON.parse that returns undefined instead of throwing — for per-line stream frames. */
|
|
92
|
+
export function safeParse(line: string): unknown {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(line) as unknown;
|
|
95
|
+
} catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Stable-enough id for a tool call a provider didn't give one for. */
|
|
101
|
+
export function randomId(): string {
|
|
102
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A stream that ended without the provider's terminal marker (an OpenAI
|
|
107
|
+
* `finish_reason`, an Anthropic `message_delta.stop_reason`, a Google
|
|
108
|
+
* `finishReason`, an Ollama `done`) was almost certainly truncated. Every
|
|
109
|
+
* engine yields this so a dropped connection is diagnosable identically
|
|
110
|
+
* regardless of provider.
|
|
111
|
+
*/
|
|
112
|
+
export function streamAnomaly(reason = 'stream ended without a terminal finish reason'): { type: 'context'; kind: string; data: unknown } {
|
|
113
|
+
return { type: 'context', kind: 'stream_anomaly', data: { reason } };
|
|
71
114
|
}
|
|
72
115
|
|
|
73
116
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
|
-
import { postResponse, sseLines } from '../../transport.js';
|
|
4
|
-
import type { ChatMessage, ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
-
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
6
|
-
import { streamError, streamTimeout } from './base.js';
|
|
3
|
+
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
+
import type { ChatMessage, ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
+
import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
6
|
+
import { DEFAULT_TIMEOUT_MS, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
7
7
|
|
|
8
8
|
export class GoogleAdapter implements ProviderAdapter {
|
|
9
9
|
readonly provider: string;
|
|
@@ -31,12 +31,23 @@ export class GoogleAdapter implements ProviderAdapter {
|
|
|
31
31
|
const toolCalls: ToolCall[] = [];
|
|
32
32
|
const timeout = streamTimeout(conn, req.signal);
|
|
33
33
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
34
|
+
// Gemini rejects `responseMimeType: application/json` combined with function
|
|
35
|
+
// declarations, so `body()` drops JSON mode when tools are present. Surface
|
|
36
|
+
// that downgrade instead of silently returning unstructured text.
|
|
37
|
+
if (jsonModeDowngraded(req)) {
|
|
38
|
+
yield {
|
|
39
|
+
type: 'context',
|
|
40
|
+
kind: 'json_mode_downgraded',
|
|
41
|
+
data: { reason: 'Gemini does not accept JSON response mode together with tools; the request was sent without JSON mode', provider: conn.provider },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
34
44
|
try {
|
|
35
45
|
const streamUrl = `${conn.baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:streamGenerateContent?alt=sse`;
|
|
36
46
|
const res = await postResponse(streamUrl, body(req), conn.headers, timeout.signal, conn.provider);
|
|
37
47
|
const contentType = res.headers.get('content-type') ?? '';
|
|
38
48
|
const chunks = contentType.includes('text/event-stream') ? sseLines(res) : singleJsonLine(res);
|
|
39
49
|
for await (const line of chunks) {
|
|
50
|
+
timeout.bump();
|
|
40
51
|
const parsed = safeParse(line);
|
|
41
52
|
const record = asRecord(parsed);
|
|
42
53
|
const promptFeedback = asRecord(record?.promptFeedback);
|
|
@@ -44,6 +55,12 @@ export class GoogleAdapter implements ProviderAdapter {
|
|
|
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) firstTokenMs = Date.now() - started;
|
|
48
65
|
text += partText;
|
|
49
66
|
yield { type: 'delta', text: partText };
|
|
@@ -65,6 +82,7 @@ export class GoogleAdapter implements ProviderAdapter {
|
|
|
65
82
|
inputTokens = asNumber(usage?.promptTokenCount) ?? inputTokens;
|
|
66
83
|
outputTokens = asNumber(usage?.candidatesTokenCount) ?? outputTokens;
|
|
67
84
|
}
|
|
85
|
+
if (!finish) yield streamAnomaly('stream ended without a finishReason');
|
|
68
86
|
const hasTools = toolCalls.length > 0;
|
|
69
87
|
yield { type: 'done', result: {
|
|
70
88
|
text,
|
|
@@ -81,6 +99,60 @@ export class GoogleAdapter implements ProviderAdapter {
|
|
|
81
99
|
timeout.done();
|
|
82
100
|
}
|
|
83
101
|
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `GoogleAdapter` has no `listModels`, so without a `health` probe
|
|
105
|
+
* `AIHandler.testConnection` would report `ok: true` without ever making a
|
|
106
|
+
* network call. `/v1beta/models` is a lightweight, key-authenticated GET
|
|
107
|
+
* that gives an honest connectivity/auth check.
|
|
108
|
+
*/
|
|
109
|
+
async health(conn: ResolvedConnection): Promise<{ ok: boolean; detail?: string }> {
|
|
110
|
+
try {
|
|
111
|
+
await fetchJson(joinUrl(conn.baseUrl, '/v1beta/models'), {
|
|
112
|
+
method: 'GET',
|
|
113
|
+
headers: conn.headers,
|
|
114
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10_000),
|
|
115
|
+
provider: conn.provider,
|
|
116
|
+
});
|
|
117
|
+
return { ok: true };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Lists models from Google's `/v1beta/models`. Strips the `models/` name
|
|
125
|
+
* prefix, and maps `inputTokenLimit` → `contextWindow` and
|
|
126
|
+
* `supportedGenerationMethods` → `capabilities`.
|
|
127
|
+
*/
|
|
128
|
+
async listModels(conn: ResolvedConnection): Promise<ModelInfo[]> {
|
|
129
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/v1beta/models'), {
|
|
130
|
+
method: 'GET',
|
|
131
|
+
headers: conn.headers,
|
|
132
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 15_000),
|
|
133
|
+
provider: conn.provider,
|
|
134
|
+
});
|
|
135
|
+
const rows = Array.isArray(asRecord(data)?.models) ? asRecord(data)!.models as unknown[] : [];
|
|
136
|
+
const models: ModelInfo[] = [];
|
|
137
|
+
for (const value of rows) {
|
|
138
|
+
const row = asRecord(value);
|
|
139
|
+
const name = asString(row?.name);
|
|
140
|
+
if (!name) continue;
|
|
141
|
+
const id = name.startsWith('models/') ? name.slice('models/'.length) : name;
|
|
142
|
+
const model: ModelInfo = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
|
|
143
|
+
const contextWindow = asNumber(row?.inputTokenLimit);
|
|
144
|
+
if (contextWindow !== undefined) model.contextWindow = contextWindow;
|
|
145
|
+
const methods = row?.supportedGenerationMethods;
|
|
146
|
+
if (Array.isArray(methods) && methods.every((m) => typeof m === 'string')) model.capabilities = methods as string[];
|
|
147
|
+
models.push(model);
|
|
148
|
+
}
|
|
149
|
+
return models;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** True when the caller asked for JSON mode but tools force it off. */
|
|
154
|
+
function jsonModeDowngraded(req: ChatRequest): boolean {
|
|
155
|
+
return req.responseFormat?.type === 'json' && !!req.tools?.length;
|
|
84
156
|
}
|
|
85
157
|
|
|
86
158
|
function body(req: ChatRequest): Record<string, unknown> {
|
|
@@ -106,7 +178,10 @@ function body(req: ChatRequest): Record<string, unknown> {
|
|
|
106
178
|
if (typeof req.toolChoice === 'object') config.allowedFunctionNames = [req.toolChoice.name];
|
|
107
179
|
payload.toolConfig = { functionCallingConfig: config };
|
|
108
180
|
}
|
|
109
|
-
|
|
181
|
+
// providerOptions reaches Google-native fields: top-level `safetySettings`,
|
|
182
|
+
// `cachedContent`, and `generationConfig` extras (`thinkingConfig`,
|
|
183
|
+
// `responseModalities`, …) merged one level deep into generationConfig.
|
|
184
|
+
return applyProviderOptions(payload, req.providerOptions, ['generationConfig']);
|
|
110
185
|
}
|
|
111
186
|
|
|
112
187
|
function toGoogleContents(messages: ChatMessage[]): Record<string, unknown>[] {
|
|
@@ -130,28 +205,27 @@ function toGoogleContents(messages: ChatMessage[]): Record<string, unknown>[] {
|
|
|
130
205
|
}
|
|
131
206
|
|
|
132
207
|
function toGoogleContent(m: ChatMessage): Record<string, unknown> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
role: 'user',
|
|
136
|
-
parts: [{ functionResponse: { name: m.name ?? 'unknown', response: toolResponseObject(m) } }],
|
|
137
|
-
};
|
|
138
|
-
}
|
|
208
|
+
// Tool-role messages are handled by toGoogleContents' batching loop and never
|
|
209
|
+
// reach here, so no tool branch is needed.
|
|
139
210
|
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
140
|
-
const parts: unknown[] = [];
|
|
141
|
-
if (typeof m.content === 'string' && m.content) parts.push({ text: m.content });
|
|
211
|
+
const parts: unknown[] = [...googleParts(m.content)];
|
|
142
212
|
for (const call of m.toolCalls) parts.push({ functionCall: { name: call.name, args: call.arguments ?? {} } });
|
|
143
213
|
return { role: 'model', parts };
|
|
144
214
|
}
|
|
145
215
|
return {
|
|
146
216
|
role: m.role === 'assistant' ? 'model' : 'user',
|
|
147
|
-
parts:
|
|
148
|
-
? [{ text: m.content }]
|
|
149
|
-
: m.content.map((part) => part.type === 'image'
|
|
150
|
-
? { inlineData: { mimeType: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
151
|
-
: { text: part.text ?? '' }),
|
|
217
|
+
parts: googleParts(m.content),
|
|
152
218
|
};
|
|
153
219
|
}
|
|
154
220
|
|
|
221
|
+
/** Maps text/image content — a string or a ContentPart[] — onto Google parts. */
|
|
222
|
+
function googleParts(content: ChatMessage['content']): unknown[] {
|
|
223
|
+
if (typeof content === 'string') return content ? [{ text: content }] : [];
|
|
224
|
+
return content.map((part) => part.type === 'image'
|
|
225
|
+
? { inlineData: { mimeType: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
226
|
+
: { text: part.text ?? '' });
|
|
227
|
+
}
|
|
228
|
+
|
|
155
229
|
function textContent(content: ChatMessage['content']): string {
|
|
156
230
|
if (typeof content === 'string') return content;
|
|
157
231
|
if (!content) return '';
|
|
@@ -196,14 +270,3 @@ function mapFinish(finish: string | undefined, hasTools: boolean): ChatResult['f
|
|
|
196
270
|
return 'stop';
|
|
197
271
|
}
|
|
198
272
|
|
|
199
|
-
function safeParse(line: string): unknown {
|
|
200
|
-
try {
|
|
201
|
-
return JSON.parse(line) as unknown;
|
|
202
|
-
} catch {
|
|
203
|
-
return undefined;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function randomId(): string {
|
|
208
|
-
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
209
|
-
}
|
|
@@ -1,9 +1,12 @@
|
|
|
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 type { ChatMessage, ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
-
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
6
|
-
import { DEFAULT_TIMEOUT_MS, streamError, streamTimeout } from './base.js';
|
|
4
|
+
import type { ChatMessage, ChatRequest, ChatResult, EmbedRequest, EmbedResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
+
import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, mapWithConcurrency, textFromMessages } from '../../util.js';
|
|
6
|
+
import { DEFAULT_TIMEOUT_MS, parseArgs, randomId, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
7
|
+
|
|
8
|
+
/** Max concurrent /api/show probes when listing models — enough to be fast, few enough to be polite to a local daemon. */
|
|
9
|
+
const SHOW_CONCURRENCY = 6;
|
|
7
10
|
|
|
8
11
|
export class OllamaAdapter implements ProviderAdapter {
|
|
9
12
|
readonly provider: string;
|
|
@@ -28,14 +31,18 @@ export class OllamaAdapter implements ProviderAdapter {
|
|
|
28
31
|
let inputTokens: number | undefined;
|
|
29
32
|
let outputTokens: number | undefined;
|
|
30
33
|
let doneReason: string | undefined;
|
|
34
|
+
let sawTerminal = false;
|
|
31
35
|
const toolCalls: ToolCall[] = [];
|
|
32
36
|
const timeout = streamTimeout(conn, req.signal);
|
|
33
37
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
34
38
|
try {
|
|
35
|
-
const res = await postResponse(
|
|
39
|
+
const res = await postResponse(joinUrl(conn.baseUrl, '/api/chat'), body(req), conn.headers, timeout.signal, conn.provider);
|
|
36
40
|
for await (const value of ndjsonLines(res)) {
|
|
41
|
+
timeout.bump();
|
|
37
42
|
const record = asRecord(value);
|
|
38
43
|
const message = asRecord(record?.message);
|
|
44
|
+
const thinking = asString(message?.thinking);
|
|
45
|
+
if (thinking) yield { type: 'reasoning', text: thinking };
|
|
39
46
|
const piece = asString(message?.content);
|
|
40
47
|
if (piece) {
|
|
41
48
|
if (firstTokenMs === null) firstTokenMs = Date.now() - started;
|
|
@@ -45,6 +52,7 @@ export class OllamaAdapter implements ProviderAdapter {
|
|
|
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) sawTerminal = true;
|
|
48
56
|
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
49
57
|
for (const callValue of calls) {
|
|
50
58
|
const fn = asRecord(asRecord(callValue)?.function);
|
|
@@ -52,13 +60,17 @@ export class OllamaAdapter implements ProviderAdapter {
|
|
|
52
60
|
const toolCall: ToolCall = {
|
|
53
61
|
id: randomId(),
|
|
54
62
|
name: asString(fn?.name) ?? 'unknown',
|
|
55
|
-
|
|
63
|
+
// Ollama itself sends a native object, but llama.cpp-style backends
|
|
64
|
+
// behind the same protocol send a JSON *string* — coerce either into
|
|
65
|
+
// the object `validateToolArgs` expects.
|
|
66
|
+
arguments: parseArgs(args),
|
|
56
67
|
raw: typeof args === 'string' ? args : JSON.stringify(args),
|
|
57
68
|
};
|
|
58
69
|
toolCalls.push(toolCall);
|
|
59
70
|
yield { type: 'tool_call', call: toolCall };
|
|
60
71
|
}
|
|
61
72
|
}
|
|
73
|
+
if (!sawTerminal) yield streamAnomaly('NDJSON stream ended without a done record');
|
|
62
74
|
const hasTools = toolCalls.length > 0;
|
|
63
75
|
yield { type: 'done', result: {
|
|
64
76
|
text,
|
|
@@ -85,17 +97,38 @@ export class OllamaAdapter implements ProviderAdapter {
|
|
|
85
97
|
});
|
|
86
98
|
const models = Array.isArray(asRecord(data)?.models) ? asRecord(data)!.models as unknown[] : [];
|
|
87
99
|
const ids = models.map((model) => asString(asRecord(model)?.name) ?? asString(asRecord(model)?.model) ?? '').filter(Boolean);
|
|
88
|
-
// Probe /api/show per model for context window + capabilities (best effort,
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
// Probe /api/show per model for context window + capabilities (best effort),
|
|
101
|
+
// with bounded concurrency so a large model list resolves quickly without
|
|
102
|
+
// opening an unbounded number of sockets against a local daemon.
|
|
103
|
+
return mapWithConcurrency(ids, SHOW_CONCURRENCY, async (id) => {
|
|
92
104
|
const info: ModelInfo = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
|
|
93
105
|
const probed = await this.showModel(conn, id).catch(() => undefined);
|
|
94
106
|
if (probed?.contextWindow !== undefined) info.contextWindow = probed.contextWindow;
|
|
95
107
|
if (probed?.capabilities) info.capabilities = probed.capabilities;
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
108
|
+
return info;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async embed(conn: ResolvedConnection, req: EmbedRequest): Promise<EmbedResult> {
|
|
113
|
+
const inputs = Array.isArray(req.input) ? req.input : [req.input];
|
|
114
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/api/embed'), {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { 'content-type': 'application/json', ...conn.headers },
|
|
117
|
+
body: JSON.stringify(applyProviderOptions({ model: req.model, input: inputs }, req.providerOptions)),
|
|
118
|
+
timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
119
|
+
provider: conn.provider,
|
|
120
|
+
});
|
|
121
|
+
const record = asRecord(data);
|
|
122
|
+
const rows = Array.isArray(record?.embeddings) ? record.embeddings : [];
|
|
123
|
+
const embeddings = rows.map((row) => (Array.isArray(row) ? row.filter((n): n is number => typeof n === 'number') : []));
|
|
124
|
+
const inputTokens = asNumber(record?.prompt_eval_count);
|
|
125
|
+
return {
|
|
126
|
+
embeddings,
|
|
127
|
+
model: req.model,
|
|
128
|
+
usage: { inputTokens, outputTokens: 0, estimated: inputTokens === undefined },
|
|
129
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
130
|
+
raw: data,
|
|
131
|
+
};
|
|
99
132
|
}
|
|
100
133
|
|
|
101
134
|
private async showModel(conn: ResolvedConnection, model: string): Promise<{ contextWindow?: number; capabilities?: string[] }> {
|
|
@@ -124,7 +157,7 @@ export class OllamaAdapter implements ProviderAdapter {
|
|
|
124
157
|
}
|
|
125
158
|
|
|
126
159
|
function body(req: ChatRequest): Record<string, unknown> {
|
|
127
|
-
|
|
160
|
+
const base: Record<string, unknown> = {
|
|
128
161
|
model: req.model,
|
|
129
162
|
messages: req.messages.map(toOllamaMessage),
|
|
130
163
|
stream: true,
|
|
@@ -132,6 +165,11 @@ function body(req: ChatRequest): Record<string, unknown> {
|
|
|
132
165
|
format: req.responseFormat?.type === 'json' ? (req.responseFormat.schema ?? 'json') : undefined,
|
|
133
166
|
tools: req.tools?.map((tool) => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })),
|
|
134
167
|
};
|
|
168
|
+
// providerOptions reaches Ollama's real request fields — `options.num_ctx`,
|
|
169
|
+
// `options.num_keep`, top-level `keep_alive`, `think`, etc. `options` is
|
|
170
|
+
// merged one level deep so num_ctx joins the samplers above rather than
|
|
171
|
+
// replacing them.
|
|
172
|
+
return applyProviderOptions(base, req.providerOptions, ['options']);
|
|
135
173
|
}
|
|
136
174
|
|
|
137
175
|
function toOllamaMessage(m: ChatMessage): Record<string, unknown> {
|
|
@@ -147,7 +185,3 @@ function toOllamaMessage(m: ChatMessage): Record<string, unknown> {
|
|
|
147
185
|
}
|
|
148
186
|
return message;
|
|
149
187
|
}
|
|
150
|
-
|
|
151
|
-
function randomId(): string {
|
|
152
|
-
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
153
|
-
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
|
-
import { postResponse, sseLines } from '../../transport.js';
|
|
4
|
-
import type { ChatMessage, ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
-
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
3
|
+
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
+
import type { ChatMessage, ChatRequest, ChatResult, EmbedRequest, EmbedResult, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
+
import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
6
6
|
import type { ProviderProfile } from '../profiles.js';
|
|
7
|
-
import { health, listOpenModels, streamError, streamTimeout } from './base.js';
|
|
7
|
+
import { DEFAULT_TIMEOUT_MS, health, listOpenModels, parseArgs, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
8
8
|
|
|
9
9
|
export class OpenAIChatAdapter implements ProviderAdapter {
|
|
10
10
|
readonly provider: string;
|
|
@@ -34,7 +34,7 @@ export class OpenAIChatAdapter implements ProviderAdapter {
|
|
|
34
34
|
const timeout = streamTimeout(conn, req.signal);
|
|
35
35
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
36
36
|
try {
|
|
37
|
-
const res = await postResponse(urlFor(conn, this.profile, req
|
|
37
|
+
const res = await postResponse(urlFor(conn, this.profile, req), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
|
|
38
38
|
const contentType = res.headers.get('content-type') ?? '';
|
|
39
39
|
if (!contentType.includes('text/event-stream')) {
|
|
40
40
|
// Server ignored stream:true (or is a buffered gateway) — recover the whole body.
|
|
@@ -47,11 +47,13 @@ export class OpenAIChatAdapter implements ProviderAdapter {
|
|
|
47
47
|
outputTokens = parsed.outputTokens;
|
|
48
48
|
finish = parsed.finish;
|
|
49
49
|
sawTerminal = true;
|
|
50
|
+
if (parsed.reasoning) yield { type: 'reasoning', text: parsed.reasoning };
|
|
50
51
|
if (text) yield { type: 'delta', text };
|
|
51
52
|
for (const call of toolCalls) yield { type: 'tool_call', call };
|
|
52
53
|
} else {
|
|
53
54
|
const partialTools = new Map<number, { id?: string; name?: string; raw: string }>();
|
|
54
55
|
for await (const line of sseLines(res)) {
|
|
56
|
+
timeout.bump();
|
|
55
57
|
const chunk = safeParse(line);
|
|
56
58
|
const record = asRecord(chunk);
|
|
57
59
|
if (!record) continue;
|
|
@@ -63,6 +65,10 @@ export class OpenAIChatAdapter implements ProviderAdapter {
|
|
|
63
65
|
finish = asString(choice?.finish_reason) ?? finish;
|
|
64
66
|
if (choice?.finish_reason) sawTerminal = true;
|
|
65
67
|
const delta = asRecord(choice?.delta);
|
|
68
|
+
// Reasoning tokens (DeepSeek `reasoning_content`, others `reasoning`)
|
|
69
|
+
// ride their own channel so they don't contaminate the answer text.
|
|
70
|
+
const reasoning = asString(delta?.reasoning_content) ?? asString(delta?.reasoning);
|
|
71
|
+
if (reasoning) yield { type: 'reasoning', text: reasoning };
|
|
66
72
|
const piece = asString(delta?.content);
|
|
67
73
|
if (piece) {
|
|
68
74
|
if (firstTokenMs === null) firstTokenMs = Date.now() - started;
|
|
@@ -89,9 +95,7 @@ export class OpenAIChatAdapter implements ProviderAdapter {
|
|
|
89
95
|
}));
|
|
90
96
|
for (const call of toolCalls) yield { type: 'tool_call', call };
|
|
91
97
|
}
|
|
92
|
-
if (!sawTerminal)
|
|
93
|
-
yield { type: 'context', kind: 'stream_anomaly', data: { reason: 'stream ended without a finish_reason' } };
|
|
94
|
-
}
|
|
98
|
+
if (!sawTerminal) yield streamAnomaly('stream ended without a finish_reason');
|
|
95
99
|
const result = makeResult(conn, req, text, toolCalls, finish, started, firstTokenMs, inputTokens, outputTokens);
|
|
96
100
|
yield { type: 'done', result };
|
|
97
101
|
} catch (error) {
|
|
@@ -108,6 +112,36 @@ export class OpenAIChatAdapter implements ProviderAdapter {
|
|
|
108
112
|
health(conn: ResolvedConnection) {
|
|
109
113
|
return health(conn, this.profile);
|
|
110
114
|
}
|
|
115
|
+
|
|
116
|
+
async embed(conn: ResolvedConnection, req: EmbedRequest): Promise<EmbedResult> {
|
|
117
|
+
const inputs = Array.isArray(req.input) ? req.input : [req.input];
|
|
118
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/embeddings'), {
|
|
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?.data) ? record.data : [];
|
|
127
|
+
// Order by the `index` each row reports, falling back to array order.
|
|
128
|
+
const byIndex = rows
|
|
129
|
+
.map((row, i) => ({ index: asNumber(asRecord(row)?.index) ?? i, embedding: asEmbedding(asRecord(row)?.embedding) }))
|
|
130
|
+
.sort((a, b) => a.index - b.index);
|
|
131
|
+
const usage = asRecord(record?.usage);
|
|
132
|
+
const inputTokens = asNumber(usage?.prompt_tokens);
|
|
133
|
+
return {
|
|
134
|
+
embeddings: byIndex.map((row) => row.embedding),
|
|
135
|
+
model: req.model,
|
|
136
|
+
usage: { inputTokens, outputTokens: 0, estimated: inputTokens === undefined },
|
|
137
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
138
|
+
raw: data,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function asEmbedding(value: unknown): number[] {
|
|
144
|
+
return Array.isArray(value) ? value.filter((n): n is number => typeof n === 'number') : [];
|
|
111
145
|
}
|
|
112
146
|
|
|
113
147
|
function openAiBody(req: ChatRequest, profile: ProviderProfile): Record<string, unknown> {
|
|
@@ -127,7 +161,12 @@ function openAiBody(req: ChatRequest, profile: ProviderProfile): Record<string,
|
|
|
127
161
|
stream_options: profile.quirks?.supportsUsageInStream ? { include_usage: true } : undefined,
|
|
128
162
|
};
|
|
129
163
|
if (req.maxTokens !== undefined) body[profile.quirks?.maxTokensParam ?? 'max_tokens'] = req.maxTokens;
|
|
130
|
-
|
|
164
|
+
// providerOptions carries OpenAI-native fields the nugget doesn't model
|
|
165
|
+
// (`reasoning_effort`, `parallel_tool_calls`, `seed`, `logprobs`, …). `apiVersion`
|
|
166
|
+
// is consumed by urlFor for Azure and stripped here so it never hits the body.
|
|
167
|
+
const { apiVersion, ...passthrough } = req.providerOptions ?? {};
|
|
168
|
+
void apiVersion;
|
|
169
|
+
return applyProviderOptions(body, Object.keys(passthrough).length ? passthrough : undefined);
|
|
131
170
|
}
|
|
132
171
|
|
|
133
172
|
function responseFormatFor(req: ChatRequest, profile: ProviderProfile): Record<string, unknown> | undefined {
|
|
@@ -152,22 +191,31 @@ function toOpenAiMessage(message: ChatMessage): Record<string, unknown> {
|
|
|
152
191
|
};
|
|
153
192
|
}
|
|
154
193
|
|
|
155
|
-
function urlFor(conn: ResolvedConnection, profile: ProviderProfile,
|
|
194
|
+
function urlFor(conn: ResolvedConnection, profile: ProviderProfile, req: ChatRequest): string {
|
|
156
195
|
if (profile.quirks?.urlTemplate) {
|
|
157
|
-
|
|
196
|
+
// `{apiVersion}` (Azure) is filled from a per-call override, then the
|
|
197
|
+
// profile default — so a retired Azure api-version is fixable without a
|
|
198
|
+
// library release: pass `providerOptions: { apiVersion: '2025-01-01-preview' }`.
|
|
199
|
+
const apiVersion = asString(req.providerOptions?.apiVersion) ?? profile.quirks.apiVersion ?? '';
|
|
200
|
+
return profile.quirks.urlTemplate
|
|
201
|
+
.replace('{baseUrl}', conn.baseUrl)
|
|
202
|
+
.replace('{model}', encodeURIComponent(req.model))
|
|
203
|
+
.replace('{apiVersion}', apiVersion);
|
|
158
204
|
}
|
|
159
|
-
return
|
|
205
|
+
return joinUrl(conn.baseUrl, '/chat/completions');
|
|
160
206
|
}
|
|
161
207
|
|
|
162
|
-
function parseOpenAiResponse(raw: unknown): { text: string; toolCalls: ToolCall[]; inputTokens?: number; outputTokens?: number; finish?: string } {
|
|
208
|
+
function parseOpenAiResponse(raw: unknown): { text: string; reasoning?: string; toolCalls: ToolCall[]; inputTokens?: number; outputTokens?: number; finish?: string } {
|
|
163
209
|
const record = asRecord(raw);
|
|
164
210
|
const usage = asRecord(record?.usage);
|
|
165
211
|
const choice = asRecord(Array.isArray(record?.choices) ? record.choices[0] : undefined);
|
|
166
212
|
const message = asRecord(choice?.message);
|
|
167
213
|
const text = asString(message?.content) ?? asString(record?.text) ?? '';
|
|
214
|
+
const reasoning = asString(message?.reasoning_content) ?? asString(message?.reasoning);
|
|
168
215
|
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
169
216
|
return {
|
|
170
217
|
text,
|
|
218
|
+
reasoning,
|
|
171
219
|
finish: asString(choice?.finish_reason),
|
|
172
220
|
toolCalls: calls.map((value) => {
|
|
173
221
|
const call = asRecord(value);
|
|
@@ -202,22 +250,3 @@ function mapFinish(finish: string | undefined, hasToolCalls: boolean): ChatResul
|
|
|
202
250
|
return 'stop';
|
|
203
251
|
}
|
|
204
252
|
|
|
205
|
-
function parseArgs(raw: string): unknown {
|
|
206
|
-
try {
|
|
207
|
-
return raw ? JSON.parse(raw) as unknown : {};
|
|
208
|
-
} catch {
|
|
209
|
-
return {};
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function safeParse(line: string): unknown {
|
|
214
|
-
try {
|
|
215
|
-
return JSON.parse(line) as unknown;
|
|
216
|
-
} catch {
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function randomId(): string {
|
|
222
|
-
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
223
|
-
}
|