babelgate 0.0.0 → 1.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.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +467 -1
- package/dist/app.js +40 -0
- package/dist/cli.js +203 -0
- package/dist/config/env-config.js +99 -0
- package/dist/index.js +60 -0
- package/dist/middleware/error-handler.js +158 -0
- package/dist/middleware/request-logger.js +66 -0
- package/dist/middleware/require-api-key.js +80 -0
- package/dist/middleware/resolve-source.js +28 -0
- package/dist/routes/proxy.js +74 -0
- package/dist/services/http-client.js +220 -0
- package/dist/services/proxy.service.js +329 -0
- package/dist/services/stream-pipeline.js +119 -0
- package/dist/services/upstream-errors.js +34 -0
- package/dist/transformers/adapters/anthropic.adapter.js +292 -0
- package/dist/transformers/adapters/anthropic.messages.js +261 -0
- package/dist/transformers/adapters/anthropic.stream.js +378 -0
- package/dist/transformers/adapters/gemini.adapter.js +418 -0
- package/dist/transformers/adapters/gemini.schema.js +61 -0
- package/dist/transformers/adapters/gemini.stream.js +341 -0
- package/dist/transformers/adapters/gemini.tools.js +246 -0
- package/dist/transformers/adapters/index.js +141 -0
- package/dist/transformers/adapters/ollama.adapter.js +288 -0
- package/dist/transformers/adapters/ollama.stream.js +294 -0
- package/dist/transformers/adapters/ollama.tools.js +107 -0
- package/dist/transformers/adapters/openai.adapter.js +460 -0
- package/dist/transformers/adapters/openai.stream.js +229 -0
- package/dist/transformers/index.js +110 -0
- package/dist/types/anthropic.types.js +2 -0
- package/dist/types/canonical.schema.js +160 -0
- package/dist/types/canonical.types.js +8 -0
- package/dist/types/gemini.types.js +2 -0
- package/dist/types/ollama.types.js +4 -0
- package/dist/types/openai.types.js +2 -0
- package/dist/utils/canonical-error.js +77 -0
- package/dist/utils/errors.js +95 -0
- package/dist/utils/ids.js +17 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/model-map.js +39 -0
- package/dist/utils/sse.js +147 -0
- package/dist/utils/stream-reassemble.js +104 -0
- package/dist/utils/token-estimate.js +36 -0
- package/dist/utils/tool-args.js +26 -0
- package/package.json +54 -7
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.anthropicAdapter = exports.ollamaAdapter = exports.geminiAdapter = exports.openaiAdapter = exports.PROVIDER_PATHS = exports.PROVIDER_ROUTES = exports.adapters = void 0;
|
|
4
|
+
exports.findSourceRoute = findSourceRoute;
|
|
5
|
+
const openai_adapter_1 = require("./openai.adapter");
|
|
6
|
+
Object.defineProperty(exports, "openaiAdapter", { enumerable: true, get: function () { return openai_adapter_1.openaiAdapter; } });
|
|
7
|
+
const gemini_adapter_1 = require("./gemini.adapter");
|
|
8
|
+
Object.defineProperty(exports, "geminiAdapter", { enumerable: true, get: function () { return gemini_adapter_1.geminiAdapter; } });
|
|
9
|
+
const ollama_adapter_1 = require("./ollama.adapter");
|
|
10
|
+
Object.defineProperty(exports, "ollamaAdapter", { enumerable: true, get: function () { return ollama_adapter_1.ollamaAdapter; } });
|
|
11
|
+
const anthropic_adapter_1 = require("./anthropic.adapter");
|
|
12
|
+
Object.defineProperty(exports, "anthropicAdapter", { enumerable: true, get: function () { return anthropic_adapter_1.anthropicAdapter; } });
|
|
13
|
+
exports.adapters = {
|
|
14
|
+
openai: openai_adapter_1.openaiAdapter,
|
|
15
|
+
gemini: gemini_adapter_1.geminiAdapter,
|
|
16
|
+
ollama: ollama_adapter_1.ollamaAdapter,
|
|
17
|
+
anthropic: anthropic_adapter_1.anthropicAdapter,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The "<model>:<method>" pair carried in a gemini chat path
|
|
21
|
+
* (/v1beta/models/<model>:<method>). The method suffix is
|
|
22
|
+
* ":"-separated; the model id may be percent-encoded and may contain
|
|
23
|
+
* "/" (e.g. tunedModels/x). Undefined when the path has no such pair.
|
|
24
|
+
*/
|
|
25
|
+
function geminiPathParts(path) {
|
|
26
|
+
let decoded = path;
|
|
27
|
+
try {
|
|
28
|
+
decoded = decodeURIComponent(path);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// malformed escapes — fall back to the raw path
|
|
32
|
+
}
|
|
33
|
+
const prefix = "/v1beta/models/";
|
|
34
|
+
if (!decoded.startsWith(prefix))
|
|
35
|
+
return undefined;
|
|
36
|
+
const rest = decoded.slice(prefix.length);
|
|
37
|
+
const colon = rest.lastIndexOf(":");
|
|
38
|
+
if (colon === -1)
|
|
39
|
+
return undefined;
|
|
40
|
+
return { model: rest.slice(0, colon), method: rest.slice(colon + 1) };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Route conventions per provider. Each provider exposes routes for every
|
|
44
|
+
* endpoint kind it supports — matched by method + pathPrefix.
|
|
45
|
+
*/
|
|
46
|
+
exports.PROVIDER_ROUTES = {
|
|
47
|
+
openai: [
|
|
48
|
+
{
|
|
49
|
+
kind: "chat",
|
|
50
|
+
method: "POST",
|
|
51
|
+
pathPrefix: "/v1/chat/completions",
|
|
52
|
+
requestPath: () => "/v1/chat/completions",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
kind: "models",
|
|
56
|
+
method: "GET",
|
|
57
|
+
pathPrefix: "/v1/models",
|
|
58
|
+
requestPath: () => "/v1/models",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
gemini: [
|
|
62
|
+
{
|
|
63
|
+
kind: "chat",
|
|
64
|
+
method: "POST",
|
|
65
|
+
pathPrefix: "/v1beta/models",
|
|
66
|
+
requestPath: (model) => `/v1beta/models/${model}:generateContent`,
|
|
67
|
+
streamRequestPath: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,
|
|
68
|
+
modelFromPath: (path) => geminiPathParts(path)?.model,
|
|
69
|
+
isStreamPath: (path) => geminiPathParts(path)?.method === "streamGenerateContent",
|
|
70
|
+
// A path without a "<m>:<method>" pair isn't claimed here — it
|
|
71
|
+
// falls through to the model-from-body flow as before.
|
|
72
|
+
isSupportedPath: (path) => {
|
|
73
|
+
const method = geminiPathParts(path)?.method;
|
|
74
|
+
return (method === undefined ||
|
|
75
|
+
method === "generateContent" ||
|
|
76
|
+
method === "streamGenerateContent");
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
kind: "models",
|
|
81
|
+
method: "GET",
|
|
82
|
+
pathPrefix: "/v1beta/models",
|
|
83
|
+
requestPath: () => "/v1beta/models",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
ollama: [
|
|
87
|
+
{
|
|
88
|
+
kind: "chat",
|
|
89
|
+
method: "POST",
|
|
90
|
+
pathPrefix: "/api/chat",
|
|
91
|
+
requestPath: () => "/api/chat",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
kind: "models",
|
|
95
|
+
method: "GET",
|
|
96
|
+
pathPrefix: "/api/tags",
|
|
97
|
+
requestPath: () => "/api/tags",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
anthropic: [
|
|
101
|
+
{
|
|
102
|
+
// Answered locally (spec §3.5) — must precede the /v1/messages
|
|
103
|
+
// chat route: it shares that prefix and routes match in order.
|
|
104
|
+
kind: "count_tokens",
|
|
105
|
+
method: "POST",
|
|
106
|
+
pathPrefix: "/v1/messages/count_tokens",
|
|
107
|
+
requestPath: () => "/v1/messages",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
kind: "chat",
|
|
111
|
+
method: "POST",
|
|
112
|
+
pathPrefix: "/v1/messages",
|
|
113
|
+
requestPath: () => "/v1/messages",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
kind: "models",
|
|
117
|
+
method: "GET",
|
|
118
|
+
pathPrefix: "/v1/models",
|
|
119
|
+
requestPath: () => "/v1/models",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Match an incoming request to a route on the given source provider
|
|
125
|
+
* (method + path prefix). Used by resolve-source middleware and
|
|
126
|
+
* proxy.service.
|
|
127
|
+
*/
|
|
128
|
+
function findSourceRoute(source, method, path) {
|
|
129
|
+
return exports.PROVIDER_ROUTES[source].find((r) => r.method === method && path.startsWith(r.pathPrefix));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Back-compat view of the chat routes — chat endpoint per provider.
|
|
133
|
+
* Prefer PROVIDER_ROUTES for new code.
|
|
134
|
+
*/
|
|
135
|
+
exports.PROVIDER_PATHS = Object.fromEntries(Object.keys(exports.PROVIDER_ROUTES).map((provider) => {
|
|
136
|
+
const chat = exports.PROVIDER_ROUTES[provider].find((r) => r.kind === "chat");
|
|
137
|
+
return [
|
|
138
|
+
provider,
|
|
139
|
+
{ pathPrefix: chat.pathPrefix, requestPath: chat.requestPath },
|
|
140
|
+
];
|
|
141
|
+
}));
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ollamaAdapter = void 0;
|
|
4
|
+
exports.toCanonicalRequest = toCanonicalRequest;
|
|
5
|
+
exports.fromCanonicalRequest = fromCanonicalRequest;
|
|
6
|
+
exports.toCanonicalResponse = toCanonicalResponse;
|
|
7
|
+
exports.fromCanonicalResponse = fromCanonicalResponse;
|
|
8
|
+
exports.toCanonicalModelList = toCanonicalModelList;
|
|
9
|
+
exports.fromCanonicalModelList = fromCanonicalModelList;
|
|
10
|
+
exports.toCanonicalError = toCanonicalError;
|
|
11
|
+
exports.fromCanonicalError = fromCanonicalError;
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const canonical_error_1 = require("../../utils/canonical-error");
|
|
14
|
+
const errors_1 = require("../../utils/errors");
|
|
15
|
+
const ids_1 = require("../../utils/ids");
|
|
16
|
+
const ollama_stream_1 = require("./ollama.stream");
|
|
17
|
+
const ollama_tools_1 = require("./ollama.tools");
|
|
18
|
+
exports.ollamaAdapter = {
|
|
19
|
+
toCanonicalRequest,
|
|
20
|
+
fromCanonicalRequest,
|
|
21
|
+
toCanonicalResponse,
|
|
22
|
+
fromCanonicalResponse,
|
|
23
|
+
toCanonicalModelList,
|
|
24
|
+
fromCanonicalModelList,
|
|
25
|
+
toCanonicalError,
|
|
26
|
+
fromCanonicalError,
|
|
27
|
+
createStreamDecoder: ollama_stream_1.createStreamDecoder,
|
|
28
|
+
createStreamEncoder: ollama_stream_1.createStreamEncoder,
|
|
29
|
+
};
|
|
30
|
+
// ============ REQUEST: Ollama → Canonical ============
|
|
31
|
+
function toCanonicalRequest(request) {
|
|
32
|
+
// Wire ids are kept when present; id-less calls draw call_0, call_1,…
|
|
33
|
+
// (docs/decisions.md "v1.1 Task 3" convention).
|
|
34
|
+
const ids = (0, ids_1.sequentialIds)();
|
|
35
|
+
// Tool results carry only `tool_name` on the wire (docs.ollama.com
|
|
36
|
+
// api.md "Chat request (With history, with tools)"); pair each one
|
|
37
|
+
// with the earliest unconsumed earlier call of that name (FIFO).
|
|
38
|
+
const pendingByName = new Map();
|
|
39
|
+
const messages = (request.messages || []).map((m) => {
|
|
40
|
+
const out = {
|
|
41
|
+
role: m.role,
|
|
42
|
+
content: extractContent(m),
|
|
43
|
+
};
|
|
44
|
+
const wireCalls = m.tool_calls;
|
|
45
|
+
if (wireCalls && wireCalls.length > 0) {
|
|
46
|
+
out.toolCalls = (0, ollama_tools_1.toCanonicalToolCalls)(wireCalls, ids);
|
|
47
|
+
for (const tc of out.toolCalls) {
|
|
48
|
+
(0, ollama_tools_1.recordCallId)(pendingByName, tc.function.name, tc.id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (typeof m.tool_name === "string" && m.tool_name.length > 0) {
|
|
52
|
+
out.toolCallId = (0, ollama_tools_1.resolveCallId)(pendingByName, m.tool_name) ?? m.tool_name;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
});
|
|
56
|
+
// Extract system prompt from first system message
|
|
57
|
+
const systemMsg = messages.find((m) => m.role === "system");
|
|
58
|
+
const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : undefined;
|
|
59
|
+
const result = {
|
|
60
|
+
model: request.model,
|
|
61
|
+
messages,
|
|
62
|
+
...(systemPrompt && { systemPrompt }),
|
|
63
|
+
};
|
|
64
|
+
// Convert options to config
|
|
65
|
+
if (request.options) {
|
|
66
|
+
const opts = request.options;
|
|
67
|
+
result.config = {
|
|
68
|
+
...(opts.temperature !== undefined && { temperature: opts.temperature }),
|
|
69
|
+
...(opts.num_predict !== undefined && { maxTokens: opts.num_predict }),
|
|
70
|
+
...(opts.top_p !== undefined && { topP: opts.top_p }),
|
|
71
|
+
...(opts.top_k !== undefined && { topK: opts.top_k }),
|
|
72
|
+
...(opts.stop && { stopSequences: opts.stop }),
|
|
73
|
+
...(opts.seed !== undefined && { seed: opts.seed }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Canonical treats absent and stream:false identically — only an
|
|
77
|
+
// explicit true survives, keeping the request round-trip idempotent.
|
|
78
|
+
if (request.stream === true) {
|
|
79
|
+
result.stream = true;
|
|
80
|
+
}
|
|
81
|
+
if (request.tools && request.tools.length > 0) {
|
|
82
|
+
result.tools = (0, ollama_tools_1.toCanonicalTools)(request.tools);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function extractContent(message) {
|
|
87
|
+
if (!message.images || message.images.length === 0) {
|
|
88
|
+
return message.content;
|
|
89
|
+
}
|
|
90
|
+
// Has images - return multipart
|
|
91
|
+
const parts = [
|
|
92
|
+
{ type: "text", text: message.content },
|
|
93
|
+
];
|
|
94
|
+
for (const img of message.images) {
|
|
95
|
+
// Ollama images can be string (base64) or Uint8Array
|
|
96
|
+
const data = typeof img === "string" ? img : Buffer.from(img).toString("base64");
|
|
97
|
+
parts.push({
|
|
98
|
+
type: "image",
|
|
99
|
+
image: {
|
|
100
|
+
data,
|
|
101
|
+
mimeType: "image/png", // Ollama doesn't specify mime type
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return parts;
|
|
106
|
+
}
|
|
107
|
+
// ============ REQUEST: Canonical → Ollama ============
|
|
108
|
+
function fromCanonicalRequest(request) {
|
|
109
|
+
// Ollama has no tool_choice parameter (api.md "Tool calling"); auto
|
|
110
|
+
// (or absent) is its only behaviour. Anything else → 400.
|
|
111
|
+
if (request.toolChoice !== undefined && request.toolChoice !== "auto") {
|
|
112
|
+
const value = typeof request.toolChoice === "string"
|
|
113
|
+
? request.toolChoice
|
|
114
|
+
: JSON.stringify(request.toolChoice);
|
|
115
|
+
throw new errors_1.ValidationError(`Ollama does not support tool_choice "${value}"`);
|
|
116
|
+
}
|
|
117
|
+
// `tool_name` on a result is looked up from the matching assistant
|
|
118
|
+
// call in the same request (Ollama carries names, not call ids).
|
|
119
|
+
// The lookup is built while walking the messages, so a tool message
|
|
120
|
+
// resolves only against calls that come BEFORE it.
|
|
121
|
+
const callNameById = new Map();
|
|
122
|
+
const messages = request.messages.map((m) => {
|
|
123
|
+
const out = convertToOllamaMessage(m, callNameById);
|
|
124
|
+
for (const tc of m.toolCalls ?? []) {
|
|
125
|
+
callNameById.set(tc.id, tc.function.name);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
});
|
|
129
|
+
const result = {
|
|
130
|
+
model: request.model,
|
|
131
|
+
messages,
|
|
132
|
+
};
|
|
133
|
+
if (request.tools && request.tools.length > 0) {
|
|
134
|
+
result.tools = (0, ollama_tools_1.toOllamaTools)(request.tools);
|
|
135
|
+
}
|
|
136
|
+
// Convert config to options
|
|
137
|
+
if (request.config) {
|
|
138
|
+
const c = request.config;
|
|
139
|
+
const options = {};
|
|
140
|
+
if (c.temperature !== undefined)
|
|
141
|
+
options.temperature = c.temperature;
|
|
142
|
+
if (c.maxTokens !== undefined)
|
|
143
|
+
options.num_predict = c.maxTokens;
|
|
144
|
+
if (c.topP !== undefined)
|
|
145
|
+
options.top_p = c.topP;
|
|
146
|
+
if (c.topK !== undefined)
|
|
147
|
+
options.top_k = c.topK;
|
|
148
|
+
if (c.stopSequences)
|
|
149
|
+
options.stop = c.stopSequences;
|
|
150
|
+
if (c.seed !== undefined)
|
|
151
|
+
options.seed = c.seed;
|
|
152
|
+
if (c.contextWindow !== undefined)
|
|
153
|
+
options.num_ctx = c.contextWindow;
|
|
154
|
+
if (Object.keys(options).length > 0) {
|
|
155
|
+
result.options = options;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Ollama streams NDJSON by default; send stream:false unless the
|
|
159
|
+
// canonical request explicitly asked for streaming.
|
|
160
|
+
result.stream = request.stream ?? false;
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
function convertToOllamaMessage(msg, callNameById) {
|
|
164
|
+
const result = {
|
|
165
|
+
role: msg.role,
|
|
166
|
+
content: "",
|
|
167
|
+
};
|
|
168
|
+
if (typeof msg.content === "string") {
|
|
169
|
+
result.content = msg.content;
|
|
170
|
+
}
|
|
171
|
+
else if (Array.isArray(msg.content)) {
|
|
172
|
+
// Extract text and images
|
|
173
|
+
const textParts = msg.content
|
|
174
|
+
.filter((p) => p.type === "text")
|
|
175
|
+
.map((p) => p.text || "")
|
|
176
|
+
.join("\n");
|
|
177
|
+
const images = msg.content
|
|
178
|
+
.filter((p) => p.type === "image" && p.image)
|
|
179
|
+
.map((p) => p.image.data);
|
|
180
|
+
result.content = textParts;
|
|
181
|
+
if (images.length > 0) {
|
|
182
|
+
result.images = images;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
186
|
+
result.tool_calls = (0, ollama_tools_1.toOllamaToolCalls)(msg.toolCalls);
|
|
187
|
+
}
|
|
188
|
+
if (msg.role === "tool") {
|
|
189
|
+
result.tool_name = (0, ollama_tools_1.resolveToolName)(callNameById, msg.toolCallId);
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
// ============ RESPONSE: Ollama → Canonical ============
|
|
194
|
+
function toCanonicalResponse(response, model, ids = (0, ids_1.createIdGenerator)()) {
|
|
195
|
+
const message = {
|
|
196
|
+
role: "assistant",
|
|
197
|
+
content: response.message?.content ?? "",
|
|
198
|
+
};
|
|
199
|
+
const wireCalls = response.message?.tool_calls;
|
|
200
|
+
if (wireCalls && wireCalls.length > 0) {
|
|
201
|
+
message.toolCalls = (0, ollama_tools_1.toCanonicalToolCalls)(wireCalls, ids);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
id: `ollama-${(0, crypto_1.randomUUID)()}`,
|
|
205
|
+
model,
|
|
206
|
+
message,
|
|
207
|
+
// A reply ending in tool calls is a "tool_call" finish — the same
|
|
208
|
+
// rule the stream decoder applies (docs/decisions.md "v1.1 Task 6").
|
|
209
|
+
finishReason: message.toolCalls
|
|
210
|
+
? "tool_call"
|
|
211
|
+
: response.done
|
|
212
|
+
? "stop"
|
|
213
|
+
: "unknown",
|
|
214
|
+
usage: {
|
|
215
|
+
inputTokens: response.prompt_eval_count ?? 0,
|
|
216
|
+
outputTokens: response.eval_count ?? 0,
|
|
217
|
+
totalTokens: (response.prompt_eval_count ?? 0) + (response.eval_count ?? 0),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// ============ RESPONSE: Canonical → Ollama ============
|
|
222
|
+
function fromCanonicalResponse(response) {
|
|
223
|
+
const content = typeof response.message.content === "string"
|
|
224
|
+
? response.message.content
|
|
225
|
+
: "";
|
|
226
|
+
const toolCalls = response.message.toolCalls;
|
|
227
|
+
// "stop" covers canonical "tool_call": recorded Ollama answers that
|
|
228
|
+
// end in tool calls report done_reason "stop".
|
|
229
|
+
const stopped = response.finishReason === "stop" ||
|
|
230
|
+
response.finishReason === "tool_call";
|
|
231
|
+
return {
|
|
232
|
+
model: response.model,
|
|
233
|
+
created_at: new Date(),
|
|
234
|
+
message: {
|
|
235
|
+
role: "assistant",
|
|
236
|
+
content,
|
|
237
|
+
...(toolCalls &&
|
|
238
|
+
toolCalls.length > 0 && {
|
|
239
|
+
tool_calls: (0, ollama_tools_1.toOllamaToolCalls)(toolCalls),
|
|
240
|
+
}),
|
|
241
|
+
},
|
|
242
|
+
done: stopped,
|
|
243
|
+
done_reason: stopped ? "stop" : "length",
|
|
244
|
+
total_duration: 0,
|
|
245
|
+
load_duration: 0,
|
|
246
|
+
prompt_eval_count: response.usage?.inputTokens ?? 0,
|
|
247
|
+
prompt_eval_duration: 0,
|
|
248
|
+
eval_count: response.usage?.outputTokens ?? 0,
|
|
249
|
+
eval_duration: 0,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function toCanonicalModelList(response) {
|
|
253
|
+
const res = response;
|
|
254
|
+
return {
|
|
255
|
+
models: (res.models ?? [])
|
|
256
|
+
.filter((m) => Boolean(m.name ?? m.model))
|
|
257
|
+
.map((m) => ({
|
|
258
|
+
id: (m.name ?? m.model),
|
|
259
|
+
// ISO timestamp → Unix seconds
|
|
260
|
+
created: m.modified_at
|
|
261
|
+
? Math.floor(Date.parse(m.modified_at) / 1000)
|
|
262
|
+
: undefined,
|
|
263
|
+
})),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function fromCanonicalModelList(list) {
|
|
267
|
+
return {
|
|
268
|
+
models: list.models.map((m) => ({
|
|
269
|
+
name: m.id,
|
|
270
|
+
model: m.id,
|
|
271
|
+
modified_at: m.created
|
|
272
|
+
? new Date(m.created * 1000).toISOString()
|
|
273
|
+
: undefined,
|
|
274
|
+
})),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// ============ ERRORS ============
|
|
278
|
+
function toCanonicalError(status, body) {
|
|
279
|
+
const err = body?.error;
|
|
280
|
+
const message = typeof err === "string" && err.length > 0
|
|
281
|
+
? err
|
|
282
|
+
: `Upstream ollama returned ${status}`;
|
|
283
|
+
return (0, canonical_error_1.makeCanonicalError)((0, canonical_error_1.kindFromStatus)(status), message);
|
|
284
|
+
}
|
|
285
|
+
function fromCanonicalError(err) {
|
|
286
|
+
return { error: err.message };
|
|
287
|
+
}
|
|
288
|
+
// Streaming lives in ./ollama.stream — re-exported on the adapter above.
|