@matthesketh/utopia-ai 0.7.1 → 0.8.1
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/dist/adapters/anthropic.cjs +3 -0
- package/dist/adapters/anthropic.d.cts +1 -1
- package/dist/adapters/anthropic.d.ts +1 -1
- package/dist/adapters/anthropic.js +3 -0
- package/dist/adapters/google.d.cts +1 -1
- package/dist/adapters/google.d.ts +1 -1
- package/dist/adapters/ollama.cjs +13 -1
- package/dist/adapters/ollama.d.cts +2 -2
- package/dist/adapters/ollama.d.ts +2 -2
- package/dist/adapters/ollama.js +13 -1
- package/dist/adapters/openai.cjs +3 -0
- package/dist/adapters/openai.d.cts +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.js +3 -0
- package/dist/ai-B65XFWfZ.d.ts +66 -0
- package/dist/ai-KmtRQe5L.d.ts +1 -1
- package/dist/ai-dyp9MfTz.d.cts +66 -0
- package/dist/index.cjs +4 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -0
- package/dist/mcp/index.cjs +106 -17
- package/dist/mcp/index.d.cts +15 -2
- package/dist/mcp/index.d.ts +15 -2
- package/dist/mcp/index.js +106 -17
- package/dist/types-BcCKlL06.d.cts +157 -0
- package/dist/types-BcCKlL06.d.ts +157 -0
- package/package.json +1 -1
|
@@ -46,6 +46,9 @@ function anthropicAdapter(config) {
|
|
|
46
46
|
'@matthesketh/utopia-ai: "@anthropic-ai/sdk" package is required for the Anthropic adapter. Install it with: npm install @anthropic-ai/sdk'
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
+
if (config.baseURL && !/^https?:$/.test(new URL(config.baseURL).protocol)) {
|
|
50
|
+
throw new Error(`Anthropic baseURL must be http(s): ${config.baseURL}`);
|
|
51
|
+
}
|
|
49
52
|
client = new AnthropicCtor({
|
|
50
53
|
apiKey: config.apiKey,
|
|
51
54
|
...config.baseURL ? { baseURL: config.baseURL } : {}
|
|
@@ -12,6 +12,9 @@ function anthropicAdapter(config) {
|
|
|
12
12
|
'@matthesketh/utopia-ai: "@anthropic-ai/sdk" package is required for the Anthropic adapter. Install it with: npm install @anthropic-ai/sdk'
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
|
+
if (config.baseURL && !/^https?:$/.test(new URL(config.baseURL).protocol)) {
|
|
16
|
+
throw new Error(`Anthropic baseURL must be http(s): ${config.baseURL}`);
|
|
17
|
+
}
|
|
15
18
|
client = new AnthropicCtor({
|
|
16
19
|
apiKey: config.apiKey,
|
|
17
20
|
...config.baseURL ? { baseURL: config.baseURL } : {}
|
package/dist/adapters/ollama.cjs
CHANGED
|
@@ -25,9 +25,18 @@ __export(ollama_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(ollama_exports);
|
|
27
27
|
var TRAILING_SLASH_RE = /\/$/;
|
|
28
|
+
var MAX_STREAM_BUFFER = 1024 * 1024;
|
|
29
|
+
function resolveBaseURL(raw) {
|
|
30
|
+
const baseURL = (raw ?? "http://localhost:11434").replace(TRAILING_SLASH_RE, "");
|
|
31
|
+
const protocol = new URL(baseURL).protocol;
|
|
32
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
33
|
+
throw new Error(`Ollama baseURL must be http(s): ${baseURL}`);
|
|
34
|
+
}
|
|
35
|
+
return baseURL;
|
|
36
|
+
}
|
|
28
37
|
var ollamaToolCallCounter = 0;
|
|
29
38
|
function ollamaAdapter(config = {}) {
|
|
30
|
-
const baseURL = (config.baseURL
|
|
39
|
+
const baseURL = resolveBaseURL(config.baseURL);
|
|
31
40
|
return {
|
|
32
41
|
async chat(request) {
|
|
33
42
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
@@ -109,6 +118,9 @@ function ollamaAdapter(config = {}) {
|
|
|
109
118
|
const { done, value } = await reader.read();
|
|
110
119
|
if (done) break;
|
|
111
120
|
buffer += decoder.decode(value, { stream: true });
|
|
121
|
+
if (buffer.length > MAX_STREAM_BUFFER) {
|
|
122
|
+
throw new Error("Ollama stream exceeded maximum buffer size without a line delimiter");
|
|
123
|
+
}
|
|
112
124
|
const lines = buffer.split("\n");
|
|
113
125
|
buffer = lines.pop() ?? "";
|
|
114
126
|
for (const line of lines) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { O as OllamaConfig, A as AIAdapter } from '../types-
|
|
1
|
+
import { O as OllamaConfig, A as AIAdapter } from '../types-BcCKlL06.cjs';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** matches a trailing slash for URL normalization. */
|
|
4
4
|
declare const TRAILING_SLASH_RE: RegExp;
|
|
5
5
|
/**
|
|
6
6
|
* Create an Ollama adapter for local models.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { O as OllamaConfig, A as AIAdapter } from '../types-
|
|
1
|
+
import { O as OllamaConfig, A as AIAdapter } from '../types-BcCKlL06.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** matches a trailing slash for URL normalization. */
|
|
4
4
|
declare const TRAILING_SLASH_RE: RegExp;
|
|
5
5
|
/**
|
|
6
6
|
* Create an Ollama adapter for local models.
|
package/dist/adapters/ollama.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
// src/adapters/ollama.ts
|
|
2
2
|
var TRAILING_SLASH_RE = /\/$/;
|
|
3
|
+
var MAX_STREAM_BUFFER = 1024 * 1024;
|
|
4
|
+
function resolveBaseURL(raw) {
|
|
5
|
+
const baseURL = (raw ?? "http://localhost:11434").replace(TRAILING_SLASH_RE, "");
|
|
6
|
+
const protocol = new URL(baseURL).protocol;
|
|
7
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
8
|
+
throw new Error(`Ollama baseURL must be http(s): ${baseURL}`);
|
|
9
|
+
}
|
|
10
|
+
return baseURL;
|
|
11
|
+
}
|
|
3
12
|
var ollamaToolCallCounter = 0;
|
|
4
13
|
function ollamaAdapter(config = {}) {
|
|
5
|
-
const baseURL = (config.baseURL
|
|
14
|
+
const baseURL = resolveBaseURL(config.baseURL);
|
|
6
15
|
return {
|
|
7
16
|
async chat(request) {
|
|
8
17
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
@@ -84,6 +93,9 @@ function ollamaAdapter(config = {}) {
|
|
|
84
93
|
const { done, value } = await reader.read();
|
|
85
94
|
if (done) break;
|
|
86
95
|
buffer += decoder.decode(value, { stream: true });
|
|
96
|
+
if (buffer.length > MAX_STREAM_BUFFER) {
|
|
97
|
+
throw new Error("Ollama stream exceeded maximum buffer size without a line delimiter");
|
|
98
|
+
}
|
|
87
99
|
const lines = buffer.split("\n");
|
|
88
100
|
buffer = lines.pop() ?? "";
|
|
89
101
|
for (const line of lines) {
|
package/dist/adapters/openai.cjs
CHANGED
|
@@ -46,6 +46,9 @@ function openaiAdapter(config) {
|
|
|
46
46
|
'@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
+
if (config.baseURL && !/^https?:$/.test(new URL(config.baseURL).protocol)) {
|
|
50
|
+
throw new Error(`OpenAI baseURL must be http(s): ${config.baseURL}`);
|
|
51
|
+
}
|
|
49
52
|
client = new OpenAICtor({
|
|
50
53
|
apiKey: config.apiKey,
|
|
51
54
|
baseURL: config.baseURL,
|
package/dist/adapters/openai.js
CHANGED
|
@@ -12,6 +12,9 @@ function openaiAdapter(config) {
|
|
|
12
12
|
'@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
|
+
if (config.baseURL && !/^https?:$/.test(new URL(config.baseURL).protocol)) {
|
|
16
|
+
throw new Error(`OpenAI baseURL must be http(s): ${config.baseURL}`);
|
|
17
|
+
}
|
|
15
18
|
client = new OpenAICtor({
|
|
16
19
|
apiKey: config.apiKey,
|
|
17
20
|
baseURL: config.baseURL,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-BcCKlL06.js';
|
|
2
|
+
|
|
3
|
+
interface AI {
|
|
4
|
+
/** Send a chat completion request. */
|
|
5
|
+
chat(request: ChatRequest): Promise<ChatResponse>;
|
|
6
|
+
/** Stream a chat completion. */
|
|
7
|
+
stream(request: ChatRequest): AsyncIterable<ChatChunk>;
|
|
8
|
+
/** Generate embeddings. */
|
|
9
|
+
embeddings(request: EmbeddingRequest): Promise<EmbeddingResponse>;
|
|
10
|
+
/**
|
|
11
|
+
* Run a tool-calling loop: send messages, execute tool calls via the
|
|
12
|
+
* provided handlers, append results, and repeat until the model stops
|
|
13
|
+
* calling tools.
|
|
14
|
+
*/
|
|
15
|
+
run(options: RunOptions): Promise<ChatResponse>;
|
|
16
|
+
}
|
|
17
|
+
interface ToolHandler {
|
|
18
|
+
definition: ToolDefinition;
|
|
19
|
+
handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
20
|
+
}
|
|
21
|
+
interface RunOptions {
|
|
22
|
+
messages: ChatMessage[];
|
|
23
|
+
tools: ToolHandler[];
|
|
24
|
+
model?: string;
|
|
25
|
+
temperature?: number;
|
|
26
|
+
maxTokens?: number;
|
|
27
|
+
maxRounds?: number;
|
|
28
|
+
onToolCall?: (call: ToolCall, result: unknown) => void;
|
|
29
|
+
extra?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
interface CreateAIOptions {
|
|
32
|
+
hooks?: AIHooks;
|
|
33
|
+
retry?: RetryConfig;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create an AI instance with the given adapter.
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { createAI } from '@matthesketh/utopia-ai';
|
|
41
|
+
* import { openaiAdapter } from '@matthesketh/utopia-ai/openai';
|
|
42
|
+
*
|
|
43
|
+
* const ai = createAI(openaiAdapter({ apiKey: process.env.OPENAI_API_KEY }));
|
|
44
|
+
*
|
|
45
|
+
* const res = await ai.chat({
|
|
46
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* // Streaming
|
|
50
|
+
* for await (const chunk of ai.stream({ messages })) {
|
|
51
|
+
* process.stdout.write(chunk.delta);
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* // Agentic tool loop
|
|
55
|
+
* const result = await ai.run({
|
|
56
|
+
* messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
57
|
+
* tools: [{
|
|
58
|
+
* definition: { name: 'get_weather', description: '...', parameters: { type: 'object', properties: {} } },
|
|
59
|
+
* handler: async ({ city }) => ({ temp: 72 }),
|
|
60
|
+
* }],
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
declare function createAI(adapter: AIAdapter, options?: CreateAIOptions): AI;
|
|
65
|
+
|
|
66
|
+
export { type AI as A, type CreateAIOptions as C, type RunOptions as R, type ToolHandler as T, createAI as c };
|
package/dist/ai-KmtRQe5L.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-FSnS43LM
|
|
1
|
+
import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-FSnS43LM';
|
|
2
2
|
|
|
3
3
|
interface AI {
|
|
4
4
|
/** Send a chat completion request. */
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-BcCKlL06.cjs';
|
|
2
|
+
|
|
3
|
+
interface AI {
|
|
4
|
+
/** Send a chat completion request. */
|
|
5
|
+
chat(request: ChatRequest): Promise<ChatResponse>;
|
|
6
|
+
/** Stream a chat completion. */
|
|
7
|
+
stream(request: ChatRequest): AsyncIterable<ChatChunk>;
|
|
8
|
+
/** Generate embeddings. */
|
|
9
|
+
embeddings(request: EmbeddingRequest): Promise<EmbeddingResponse>;
|
|
10
|
+
/**
|
|
11
|
+
* Run a tool-calling loop: send messages, execute tool calls via the
|
|
12
|
+
* provided handlers, append results, and repeat until the model stops
|
|
13
|
+
* calling tools.
|
|
14
|
+
*/
|
|
15
|
+
run(options: RunOptions): Promise<ChatResponse>;
|
|
16
|
+
}
|
|
17
|
+
interface ToolHandler {
|
|
18
|
+
definition: ToolDefinition;
|
|
19
|
+
handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
20
|
+
}
|
|
21
|
+
interface RunOptions {
|
|
22
|
+
messages: ChatMessage[];
|
|
23
|
+
tools: ToolHandler[];
|
|
24
|
+
model?: string;
|
|
25
|
+
temperature?: number;
|
|
26
|
+
maxTokens?: number;
|
|
27
|
+
maxRounds?: number;
|
|
28
|
+
onToolCall?: (call: ToolCall, result: unknown) => void;
|
|
29
|
+
extra?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
interface CreateAIOptions {
|
|
32
|
+
hooks?: AIHooks;
|
|
33
|
+
retry?: RetryConfig;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create an AI instance with the given adapter.
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { createAI } from '@matthesketh/utopia-ai';
|
|
41
|
+
* import { openaiAdapter } from '@matthesketh/utopia-ai/openai';
|
|
42
|
+
*
|
|
43
|
+
* const ai = createAI(openaiAdapter({ apiKey: process.env.OPENAI_API_KEY }));
|
|
44
|
+
*
|
|
45
|
+
* const res = await ai.chat({
|
|
46
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* // Streaming
|
|
50
|
+
* for await (const chunk of ai.stream({ messages })) {
|
|
51
|
+
* process.stdout.write(chunk.delta);
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* // Agentic tool loop
|
|
55
|
+
* const result = await ai.run({
|
|
56
|
+
* messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
57
|
+
* tools: [{
|
|
58
|
+
* definition: { name: 'get_weather', description: '...', parameters: { type: 'object', properties: {} } },
|
|
59
|
+
* handler: async ({ city }) => ({ temp: 72 }),
|
|
60
|
+
* }],
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
declare function createAI(adapter: AIAdapter, options?: CreateAIOptions): AI;
|
|
65
|
+
|
|
66
|
+
export { type AI as A, type CreateAIOptions as C, type RunOptions as R, type ToolHandler as T, createAI as c };
|
package/dist/index.cjs
CHANGED
|
@@ -209,6 +209,7 @@ async function* chatToStream(adapter, request) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
// src/streaming.ts
|
|
212
|
+
var MAX_SSE_BUFFER = 1024 * 1024;
|
|
212
213
|
async function streamSSE(res, stream, options) {
|
|
213
214
|
res.writeHead(200, {
|
|
214
215
|
"Content-Type": "text/event-stream",
|
|
@@ -247,6 +248,9 @@ async function* parseSSEStream(response) {
|
|
|
247
248
|
const { done, value } = await reader.read();
|
|
248
249
|
if (done) break;
|
|
249
250
|
buffer += decoder.decode(value, { stream: true });
|
|
251
|
+
if (buffer.length > MAX_SSE_BUFFER) {
|
|
252
|
+
throw new Error("SSE stream exceeded maximum buffer size without a line delimiter");
|
|
253
|
+
}
|
|
250
254
|
const lines = buffer.split("\n");
|
|
251
255
|
buffer = lines.pop() ?? "";
|
|
252
256
|
for (const line of lines) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { A as AI, C as CreateAIOptions, R as RunOptions, T as ToolHandler, c as createAI } from './ai-
|
|
1
|
+
export { A as AI, C as CreateAIOptions, R as RunOptions, T as ToolHandler, c as createAI } from './ai-dyp9MfTz.cjs';
|
|
2
2
|
import { ServerResponse } from 'node:http';
|
|
3
|
-
import { C as ChatChunk } from './types-
|
|
4
|
-
export { A as AIAdapter, a as AIHooks, b as AnthropicConfig, c as ChatMessage, d as ChatRequest, e as ChatResponse, E as EmbeddingRequest, f as EmbeddingResponse, G as GoogleConfig, I as ImageContent, J as JsonSchema, M as MessageContent, g as MessageRole, O as OllamaConfig, h as OpenAIConfig, R as RetryConfig, T as TextContent, i as TokenUsage, j as ToolCall, k as ToolCallContent, l as ToolDefinition, m as ToolResultContent } from './types-
|
|
3
|
+
import { C as ChatChunk } from './types-BcCKlL06.cjs';
|
|
4
|
+
export { A as AIAdapter, a as AIHooks, b as AnthropicConfig, c as ChatMessage, d as ChatRequest, e as ChatResponse, E as EmbeddingRequest, f as EmbeddingResponse, G as GoogleConfig, I as ImageContent, J as JsonSchema, M as MessageContent, g as MessageRole, O as OllamaConfig, h as OpenAIConfig, R as RetryConfig, T as TextContent, i as TokenUsage, j as ToolCall, k as ToolCallContent, l as ToolDefinition, m as ToolResultContent } from './types-BcCKlL06.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Stream AI chat chunks as Server-Sent Events (SSE).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { A as AI, C as CreateAIOptions, R as RunOptions, T as ToolHandler, c as createAI } from './ai-
|
|
1
|
+
export { A as AI, C as CreateAIOptions, R as RunOptions, T as ToolHandler, c as createAI } from './ai-B65XFWfZ.js';
|
|
2
2
|
import { ServerResponse } from 'node:http';
|
|
3
|
-
import { C as ChatChunk } from './types-
|
|
4
|
-
export { A as AIAdapter, a as AIHooks, b as AnthropicConfig, c as ChatMessage, d as ChatRequest, e as ChatResponse, E as EmbeddingRequest, f as EmbeddingResponse, G as GoogleConfig, I as ImageContent, J as JsonSchema, M as MessageContent, g as MessageRole, O as OllamaConfig, h as OpenAIConfig, R as RetryConfig, T as TextContent, i as TokenUsage, j as ToolCall, k as ToolCallContent, l as ToolDefinition, m as ToolResultContent } from './types-
|
|
3
|
+
import { C as ChatChunk } from './types-BcCKlL06.js';
|
|
4
|
+
export { A as AIAdapter, a as AIHooks, b as AnthropicConfig, c as ChatMessage, d as ChatRequest, e as ChatResponse, E as EmbeddingRequest, f as EmbeddingResponse, G as GoogleConfig, I as ImageContent, J as JsonSchema, M as MessageContent, g as MessageRole, O as OllamaConfig, h as OpenAIConfig, R as RetryConfig, T as TextContent, i as TokenUsage, j as ToolCall, k as ToolCallContent, l as ToolDefinition, m as ToolResultContent } from './types-BcCKlL06.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Stream AI chat chunks as Server-Sent Events (SSE).
|
package/dist/index.js
CHANGED
|
@@ -180,6 +180,7 @@ async function* chatToStream(adapter, request) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// src/streaming.ts
|
|
183
|
+
var MAX_SSE_BUFFER = 1024 * 1024;
|
|
183
184
|
async function streamSSE(res, stream, options) {
|
|
184
185
|
res.writeHead(200, {
|
|
185
186
|
"Content-Type": "text/event-stream",
|
|
@@ -218,6 +219,9 @@ async function* parseSSEStream(response) {
|
|
|
218
219
|
const { done, value } = await reader.read();
|
|
219
220
|
if (done) break;
|
|
220
221
|
buffer += decoder.decode(value, { stream: true });
|
|
222
|
+
if (buffer.length > MAX_SSE_BUFFER) {
|
|
223
|
+
throw new Error("SSE stream exceeded maximum buffer size without a line delimiter");
|
|
224
|
+
}
|
|
221
225
|
const lines = buffer.split("\n");
|
|
222
226
|
buffer = lines.pop() ?? "";
|
|
223
227
|
for (const line of lines) {
|
package/dist/mcp/index.cjs
CHANGED
|
@@ -58,14 +58,17 @@ function createMCPServer(config) {
|
|
|
58
58
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
59
59
|
} catch (err) {
|
|
60
60
|
const rpcErr = err;
|
|
61
|
+
if (typeof rpcErr.code === "number") {
|
|
62
|
+
return {
|
|
63
|
+
jsonrpc: "2.0",
|
|
64
|
+
id: request.id,
|
|
65
|
+
error: { code: rpcErr.code, message: rpcErr.message ?? "Error", data: rpcErr.data }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
61
68
|
return {
|
|
62
69
|
jsonrpc: "2.0",
|
|
63
70
|
id: request.id,
|
|
64
|
-
error: {
|
|
65
|
-
code: rpcErr.code ?? -32603,
|
|
66
|
-
message: rpcErr.message ?? "Internal error",
|
|
67
|
-
data: rpcErr.data
|
|
68
|
-
}
|
|
71
|
+
error: { code: -32603, message: "Internal error" }
|
|
69
72
|
};
|
|
70
73
|
}
|
|
71
74
|
}
|
|
@@ -93,12 +96,22 @@ function createMCPServer(config) {
|
|
|
93
96
|
}))
|
|
94
97
|
};
|
|
95
98
|
case "tools/call": {
|
|
96
|
-
const params = request.params;
|
|
97
|
-
const
|
|
99
|
+
const params = asParamsObject(request.params, "tools/call");
|
|
100
|
+
const name = params.name;
|
|
101
|
+
if (typeof name !== "string") {
|
|
102
|
+
throw makeError(-32602, 'tools/call requires a string "name"');
|
|
103
|
+
}
|
|
104
|
+
const tool = toolMap.get(name);
|
|
98
105
|
if (!tool) {
|
|
99
|
-
throw makeError(-32602, `Unknown tool: ${
|
|
106
|
+
throw makeError(-32602, `Unknown tool: ${name}`);
|
|
100
107
|
}
|
|
101
|
-
|
|
108
|
+
const args = params.arguments ?? {};
|
|
109
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
110
|
+
throw makeError(-32602, "tool arguments must be an object");
|
|
111
|
+
}
|
|
112
|
+
const argRecord = args;
|
|
113
|
+
validateAgainstSchema(tool.definition.inputSchema, argRecord, name);
|
|
114
|
+
return tool.handler(argRecord);
|
|
102
115
|
}
|
|
103
116
|
case "resources/list":
|
|
104
117
|
return {
|
|
@@ -110,12 +123,16 @@ function createMCPServer(config) {
|
|
|
110
123
|
}))
|
|
111
124
|
};
|
|
112
125
|
case "resources/read": {
|
|
113
|
-
const params = request.params;
|
|
114
|
-
const
|
|
126
|
+
const params = asParamsObject(request.params, "resources/read");
|
|
127
|
+
const uri = params.uri;
|
|
128
|
+
if (typeof uri !== "string") {
|
|
129
|
+
throw makeError(-32602, 'resources/read requires a string "uri"');
|
|
130
|
+
}
|
|
131
|
+
const resource = findResource(uri);
|
|
115
132
|
if (!resource) {
|
|
116
|
-
throw makeError(-32602, `Unknown resource: ${
|
|
133
|
+
throw makeError(-32602, `Unknown resource: ${uri}`);
|
|
117
134
|
}
|
|
118
|
-
const content = await resource.handler(
|
|
135
|
+
const content = await resource.handler(uri);
|
|
119
136
|
return { contents: [content] };
|
|
120
137
|
}
|
|
121
138
|
case "prompts/list":
|
|
@@ -127,12 +144,17 @@ function createMCPServer(config) {
|
|
|
127
144
|
}))
|
|
128
145
|
};
|
|
129
146
|
case "prompts/get": {
|
|
130
|
-
const params = request.params;
|
|
131
|
-
const
|
|
147
|
+
const params = asParamsObject(request.params, "prompts/get");
|
|
148
|
+
const name = params.name;
|
|
149
|
+
if (typeof name !== "string") {
|
|
150
|
+
throw makeError(-32602, 'prompts/get requires a string "name"');
|
|
151
|
+
}
|
|
152
|
+
const prompt = promptMap.get(name);
|
|
132
153
|
if (!prompt) {
|
|
133
|
-
throw makeError(-32602, `Unknown prompt: ${
|
|
154
|
+
throw makeError(-32602, `Unknown prompt: ${name}`);
|
|
134
155
|
}
|
|
135
|
-
|
|
156
|
+
const args = params.arguments ?? {};
|
|
157
|
+
return prompt.handler(args);
|
|
136
158
|
}
|
|
137
159
|
case "ping":
|
|
138
160
|
return {};
|
|
@@ -159,10 +181,46 @@ function matchesTemplate(pattern, uri) {
|
|
|
159
181
|
function makeError(code, message) {
|
|
160
182
|
return { code, message };
|
|
161
183
|
}
|
|
184
|
+
function asParamsObject(params, ctx) {
|
|
185
|
+
if (params === null || typeof params !== "object" || Array.isArray(params)) {
|
|
186
|
+
throw makeError(-32602, `Invalid params for ${ctx}: expected an object`);
|
|
187
|
+
}
|
|
188
|
+
return params;
|
|
189
|
+
}
|
|
190
|
+
function validateAgainstSchema(schema, args, toolName) {
|
|
191
|
+
if (!schema || typeof schema !== "object") return;
|
|
192
|
+
const s = schema;
|
|
193
|
+
if (Array.isArray(s.required)) {
|
|
194
|
+
for (const key of s.required) {
|
|
195
|
+
if (typeof key === "string" && !(key in args)) {
|
|
196
|
+
throw makeError(-32602, `Missing required argument "${key}" for tool "${toolName}"`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (s.properties && typeof s.properties === "object") {
|
|
201
|
+
for (const [key, prop] of Object.entries(s.properties)) {
|
|
202
|
+
const value = args[key];
|
|
203
|
+
if (value === void 0 || value === null) continue;
|
|
204
|
+
const expected = prop?.type;
|
|
205
|
+
if (!expected) continue;
|
|
206
|
+
const ok = expected === "number" || expected === "integer" ? typeof value === "number" : expected === "array" ? Array.isArray(value) : expected === "object" ? typeof value === "object" && !Array.isArray(value) : typeof value === expected;
|
|
207
|
+
if (!ok) {
|
|
208
|
+
throw makeError(
|
|
209
|
+
-32602,
|
|
210
|
+
`Argument "${key}" for tool "${toolName}" must be of type ${expected}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
162
216
|
|
|
163
217
|
// src/mcp/client.ts
|
|
164
218
|
function createMCPClient(config) {
|
|
165
219
|
let requestId = 0;
|
|
220
|
+
const parsedUrl = new URL(config.url);
|
|
221
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
222
|
+
throw new Error(`MCP client URL must be http(s): ${config.url}`);
|
|
223
|
+
}
|
|
166
224
|
async function rpc(method, params) {
|
|
167
225
|
const request = {
|
|
168
226
|
jsonrpc: "2.0",
|
|
@@ -177,6 +235,10 @@ function createMCPClient(config) {
|
|
|
177
235
|
...config.headers
|
|
178
236
|
},
|
|
179
237
|
body: JSON.stringify(request),
|
|
238
|
+
// never follow redirects: a 3xx to another host would otherwise re-send
|
|
239
|
+
// config.headers (often a bearer token) to that host, leaking the
|
|
240
|
+
// credential and enabling an ssrf pivot.
|
|
241
|
+
redirect: "error",
|
|
180
242
|
signal: AbortSignal.timeout(3e4)
|
|
181
243
|
});
|
|
182
244
|
if (!response.ok) {
|
|
@@ -250,6 +312,8 @@ function createMCPClient(config) {
|
|
|
250
312
|
// src/mcp/handler.ts
|
|
251
313
|
function createMCPHandler(server, options) {
|
|
252
314
|
const corsOrigin = options?.corsOrigin;
|
|
315
|
+
const allowedOrigins = options?.allowedOrigins;
|
|
316
|
+
const authorize = options?.authorize;
|
|
253
317
|
return async (req, res) => {
|
|
254
318
|
if (corsOrigin) {
|
|
255
319
|
res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
|
@@ -261,6 +325,31 @@ function createMCPHandler(server, options) {
|
|
|
261
325
|
res.end();
|
|
262
326
|
return;
|
|
263
327
|
}
|
|
328
|
+
const origin = req.headers.origin;
|
|
329
|
+
if (allowedOrigins && origin !== void 0 && !allowedOrigins.includes(origin)) {
|
|
330
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
331
|
+
res.end("Forbidden");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (authorize) {
|
|
335
|
+
let allowed = false;
|
|
336
|
+
try {
|
|
337
|
+
allowed = await authorize(req);
|
|
338
|
+
} catch {
|
|
339
|
+
allowed = false;
|
|
340
|
+
}
|
|
341
|
+
if (!allowed) {
|
|
342
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
343
|
+
res.end(
|
|
344
|
+
JSON.stringify({
|
|
345
|
+
jsonrpc: "2.0",
|
|
346
|
+
id: null,
|
|
347
|
+
error: { code: -32600, message: "Unauthorized" }
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
264
353
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
265
354
|
if (url.pathname.endsWith("/sse") && req.method === "GET") {
|
|
266
355
|
handleSSE(server, req, res);
|
package/dist/mcp/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { J as JsonSchema } from '../types-
|
|
2
|
-
import { T as ToolHandler } from '../ai-
|
|
1
|
+
import { J as JsonSchema } from '../types-BcCKlL06.cjs';
|
|
2
|
+
import { T as ToolHandler } from '../ai-dyp9MfTz.cjs';
|
|
3
3
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
4
|
|
|
5
5
|
interface JsonRpcRequest {
|
|
@@ -174,6 +174,19 @@ declare function createMCPClient(config: MCPClientConfig): MCPClient;
|
|
|
174
174
|
|
|
175
175
|
interface MCPHandlerOptions {
|
|
176
176
|
corsOrigin?: string;
|
|
177
|
+
/**
|
|
178
|
+
* allow-list of permitted `Origin` header values. when set, any request
|
|
179
|
+
* carrying an `Origin` not in the list is rejected with 403. this is the
|
|
180
|
+
* primary defence against dns-rebinding attacks on a locally-bound server.
|
|
181
|
+
*/
|
|
182
|
+
allowedOrigins?: string[];
|
|
183
|
+
/**
|
|
184
|
+
* authorisation gate run before any request is dispatched to the server.
|
|
185
|
+
* return false (or throw) to reject with 401. an mcp server exposes tool
|
|
186
|
+
* execution, so it must not be reachable on a network without an authz
|
|
187
|
+
* check — there is intentionally no default-allow for remote callers.
|
|
188
|
+
*/
|
|
189
|
+
authorize?: (req: IncomingMessage) => boolean | Promise<boolean>;
|
|
177
190
|
}
|
|
178
191
|
/**
|
|
179
192
|
* Create a Node.js HTTP handler for an MCP server.
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { J as JsonSchema } from '../types-
|
|
2
|
-
import { T as ToolHandler } from '../ai-
|
|
1
|
+
import { J as JsonSchema } from '../types-BcCKlL06.js';
|
|
2
|
+
import { T as ToolHandler } from '../ai-B65XFWfZ.js';
|
|
3
3
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
4
|
|
|
5
5
|
interface JsonRpcRequest {
|
|
@@ -174,6 +174,19 @@ declare function createMCPClient(config: MCPClientConfig): MCPClient;
|
|
|
174
174
|
|
|
175
175
|
interface MCPHandlerOptions {
|
|
176
176
|
corsOrigin?: string;
|
|
177
|
+
/**
|
|
178
|
+
* allow-list of permitted `Origin` header values. when set, any request
|
|
179
|
+
* carrying an `Origin` not in the list is rejected with 403. this is the
|
|
180
|
+
* primary defence against dns-rebinding attacks on a locally-bound server.
|
|
181
|
+
*/
|
|
182
|
+
allowedOrigins?: string[];
|
|
183
|
+
/**
|
|
184
|
+
* authorisation gate run before any request is dispatched to the server.
|
|
185
|
+
* return false (or throw) to reject with 401. an mcp server exposes tool
|
|
186
|
+
* execution, so it must not be reachable on a network without an authz
|
|
187
|
+
* check — there is intentionally no default-allow for remote callers.
|
|
188
|
+
*/
|
|
189
|
+
authorize?: (req: IncomingMessage) => boolean | Promise<boolean>;
|
|
177
190
|
}
|
|
178
191
|
/**
|
|
179
192
|
* Create a Node.js HTTP handler for an MCP server.
|
package/dist/mcp/index.js
CHANGED
|
@@ -30,14 +30,17 @@ function createMCPServer(config) {
|
|
|
30
30
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
31
31
|
} catch (err) {
|
|
32
32
|
const rpcErr = err;
|
|
33
|
+
if (typeof rpcErr.code === "number") {
|
|
34
|
+
return {
|
|
35
|
+
jsonrpc: "2.0",
|
|
36
|
+
id: request.id,
|
|
37
|
+
error: { code: rpcErr.code, message: rpcErr.message ?? "Error", data: rpcErr.data }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
33
40
|
return {
|
|
34
41
|
jsonrpc: "2.0",
|
|
35
42
|
id: request.id,
|
|
36
|
-
error: {
|
|
37
|
-
code: rpcErr.code ?? -32603,
|
|
38
|
-
message: rpcErr.message ?? "Internal error",
|
|
39
|
-
data: rpcErr.data
|
|
40
|
-
}
|
|
43
|
+
error: { code: -32603, message: "Internal error" }
|
|
41
44
|
};
|
|
42
45
|
}
|
|
43
46
|
}
|
|
@@ -65,12 +68,22 @@ function createMCPServer(config) {
|
|
|
65
68
|
}))
|
|
66
69
|
};
|
|
67
70
|
case "tools/call": {
|
|
68
|
-
const params = request.params;
|
|
69
|
-
const
|
|
71
|
+
const params = asParamsObject(request.params, "tools/call");
|
|
72
|
+
const name = params.name;
|
|
73
|
+
if (typeof name !== "string") {
|
|
74
|
+
throw makeError(-32602, 'tools/call requires a string "name"');
|
|
75
|
+
}
|
|
76
|
+
const tool = toolMap.get(name);
|
|
70
77
|
if (!tool) {
|
|
71
|
-
throw makeError(-32602, `Unknown tool: ${
|
|
78
|
+
throw makeError(-32602, `Unknown tool: ${name}`);
|
|
72
79
|
}
|
|
73
|
-
|
|
80
|
+
const args = params.arguments ?? {};
|
|
81
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
82
|
+
throw makeError(-32602, "tool arguments must be an object");
|
|
83
|
+
}
|
|
84
|
+
const argRecord = args;
|
|
85
|
+
validateAgainstSchema(tool.definition.inputSchema, argRecord, name);
|
|
86
|
+
return tool.handler(argRecord);
|
|
74
87
|
}
|
|
75
88
|
case "resources/list":
|
|
76
89
|
return {
|
|
@@ -82,12 +95,16 @@ function createMCPServer(config) {
|
|
|
82
95
|
}))
|
|
83
96
|
};
|
|
84
97
|
case "resources/read": {
|
|
85
|
-
const params = request.params;
|
|
86
|
-
const
|
|
98
|
+
const params = asParamsObject(request.params, "resources/read");
|
|
99
|
+
const uri = params.uri;
|
|
100
|
+
if (typeof uri !== "string") {
|
|
101
|
+
throw makeError(-32602, 'resources/read requires a string "uri"');
|
|
102
|
+
}
|
|
103
|
+
const resource = findResource(uri);
|
|
87
104
|
if (!resource) {
|
|
88
|
-
throw makeError(-32602, `Unknown resource: ${
|
|
105
|
+
throw makeError(-32602, `Unknown resource: ${uri}`);
|
|
89
106
|
}
|
|
90
|
-
const content = await resource.handler(
|
|
107
|
+
const content = await resource.handler(uri);
|
|
91
108
|
return { contents: [content] };
|
|
92
109
|
}
|
|
93
110
|
case "prompts/list":
|
|
@@ -99,12 +116,17 @@ function createMCPServer(config) {
|
|
|
99
116
|
}))
|
|
100
117
|
};
|
|
101
118
|
case "prompts/get": {
|
|
102
|
-
const params = request.params;
|
|
103
|
-
const
|
|
119
|
+
const params = asParamsObject(request.params, "prompts/get");
|
|
120
|
+
const name = params.name;
|
|
121
|
+
if (typeof name !== "string") {
|
|
122
|
+
throw makeError(-32602, 'prompts/get requires a string "name"');
|
|
123
|
+
}
|
|
124
|
+
const prompt = promptMap.get(name);
|
|
104
125
|
if (!prompt) {
|
|
105
|
-
throw makeError(-32602, `Unknown prompt: ${
|
|
126
|
+
throw makeError(-32602, `Unknown prompt: ${name}`);
|
|
106
127
|
}
|
|
107
|
-
|
|
128
|
+
const args = params.arguments ?? {};
|
|
129
|
+
return prompt.handler(args);
|
|
108
130
|
}
|
|
109
131
|
case "ping":
|
|
110
132
|
return {};
|
|
@@ -131,10 +153,46 @@ function matchesTemplate(pattern, uri) {
|
|
|
131
153
|
function makeError(code, message) {
|
|
132
154
|
return { code, message };
|
|
133
155
|
}
|
|
156
|
+
function asParamsObject(params, ctx) {
|
|
157
|
+
if (params === null || typeof params !== "object" || Array.isArray(params)) {
|
|
158
|
+
throw makeError(-32602, `Invalid params for ${ctx}: expected an object`);
|
|
159
|
+
}
|
|
160
|
+
return params;
|
|
161
|
+
}
|
|
162
|
+
function validateAgainstSchema(schema, args, toolName) {
|
|
163
|
+
if (!schema || typeof schema !== "object") return;
|
|
164
|
+
const s = schema;
|
|
165
|
+
if (Array.isArray(s.required)) {
|
|
166
|
+
for (const key of s.required) {
|
|
167
|
+
if (typeof key === "string" && !(key in args)) {
|
|
168
|
+
throw makeError(-32602, `Missing required argument "${key}" for tool "${toolName}"`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (s.properties && typeof s.properties === "object") {
|
|
173
|
+
for (const [key, prop] of Object.entries(s.properties)) {
|
|
174
|
+
const value = args[key];
|
|
175
|
+
if (value === void 0 || value === null) continue;
|
|
176
|
+
const expected = prop?.type;
|
|
177
|
+
if (!expected) continue;
|
|
178
|
+
const ok = expected === "number" || expected === "integer" ? typeof value === "number" : expected === "array" ? Array.isArray(value) : expected === "object" ? typeof value === "object" && !Array.isArray(value) : typeof value === expected;
|
|
179
|
+
if (!ok) {
|
|
180
|
+
throw makeError(
|
|
181
|
+
-32602,
|
|
182
|
+
`Argument "${key}" for tool "${toolName}" must be of type ${expected}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
134
188
|
|
|
135
189
|
// src/mcp/client.ts
|
|
136
190
|
function createMCPClient(config) {
|
|
137
191
|
let requestId = 0;
|
|
192
|
+
const parsedUrl = new URL(config.url);
|
|
193
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
194
|
+
throw new Error(`MCP client URL must be http(s): ${config.url}`);
|
|
195
|
+
}
|
|
138
196
|
async function rpc(method, params) {
|
|
139
197
|
const request = {
|
|
140
198
|
jsonrpc: "2.0",
|
|
@@ -149,6 +207,10 @@ function createMCPClient(config) {
|
|
|
149
207
|
...config.headers
|
|
150
208
|
},
|
|
151
209
|
body: JSON.stringify(request),
|
|
210
|
+
// never follow redirects: a 3xx to another host would otherwise re-send
|
|
211
|
+
// config.headers (often a bearer token) to that host, leaking the
|
|
212
|
+
// credential and enabling an ssrf pivot.
|
|
213
|
+
redirect: "error",
|
|
152
214
|
signal: AbortSignal.timeout(3e4)
|
|
153
215
|
});
|
|
154
216
|
if (!response.ok) {
|
|
@@ -222,6 +284,8 @@ function createMCPClient(config) {
|
|
|
222
284
|
// src/mcp/handler.ts
|
|
223
285
|
function createMCPHandler(server, options) {
|
|
224
286
|
const corsOrigin = options?.corsOrigin;
|
|
287
|
+
const allowedOrigins = options?.allowedOrigins;
|
|
288
|
+
const authorize = options?.authorize;
|
|
225
289
|
return async (req, res) => {
|
|
226
290
|
if (corsOrigin) {
|
|
227
291
|
res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
|
@@ -233,6 +297,31 @@ function createMCPHandler(server, options) {
|
|
|
233
297
|
res.end();
|
|
234
298
|
return;
|
|
235
299
|
}
|
|
300
|
+
const origin = req.headers.origin;
|
|
301
|
+
if (allowedOrigins && origin !== void 0 && !allowedOrigins.includes(origin)) {
|
|
302
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
303
|
+
res.end("Forbidden");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (authorize) {
|
|
307
|
+
let allowed = false;
|
|
308
|
+
try {
|
|
309
|
+
allowed = await authorize(req);
|
|
310
|
+
} catch {
|
|
311
|
+
allowed = false;
|
|
312
|
+
}
|
|
313
|
+
if (!allowed) {
|
|
314
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
315
|
+
res.end(
|
|
316
|
+
JSON.stringify({
|
|
317
|
+
jsonrpc: "2.0",
|
|
318
|
+
id: null,
|
|
319
|
+
error: { code: -32600, message: "Unauthorized" }
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
236
325
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
237
326
|
if (url.pathname.endsWith("/sse") && req.method === "GET") {
|
|
238
327
|
handleSSE(server, req, res);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
2
|
+
interface TextContent {
|
|
3
|
+
type: 'text';
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
interface ImageContent {
|
|
7
|
+
type: 'image';
|
|
8
|
+
/** Base64-encoded image data or a URL. */
|
|
9
|
+
source: string;
|
|
10
|
+
mediaType?: string;
|
|
11
|
+
}
|
|
12
|
+
interface ToolCallContent {
|
|
13
|
+
type: 'tool_call';
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
arguments: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
interface ToolResultContent {
|
|
19
|
+
type: 'tool_result';
|
|
20
|
+
id: string;
|
|
21
|
+
content: string;
|
|
22
|
+
isError?: boolean;
|
|
23
|
+
}
|
|
24
|
+
type MessageContent = string | TextContent | ImageContent | ToolCallContent | ToolResultContent;
|
|
25
|
+
interface ChatMessage {
|
|
26
|
+
role: MessageRole;
|
|
27
|
+
content: MessageContent | MessageContent[];
|
|
28
|
+
name?: string;
|
|
29
|
+
}
|
|
30
|
+
interface ToolDefinition {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
parameters: JsonSchema;
|
|
34
|
+
}
|
|
35
|
+
interface JsonSchema {
|
|
36
|
+
type: string;
|
|
37
|
+
properties?: Record<string, JsonSchema & {
|
|
38
|
+
description?: string;
|
|
39
|
+
}>;
|
|
40
|
+
required?: string[];
|
|
41
|
+
items?: JsonSchema;
|
|
42
|
+
enum?: unknown[];
|
|
43
|
+
description?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
interface ChatRequest {
|
|
47
|
+
messages: ChatMessage[];
|
|
48
|
+
model?: string;
|
|
49
|
+
temperature?: number;
|
|
50
|
+
maxTokens?: number;
|
|
51
|
+
topP?: number;
|
|
52
|
+
stop?: string[];
|
|
53
|
+
tools?: ToolDefinition[];
|
|
54
|
+
toolChoice?: 'auto' | 'none' | 'required' | {
|
|
55
|
+
name: string;
|
|
56
|
+
};
|
|
57
|
+
/** Adapter-specific options passed through untouched. */
|
|
58
|
+
extra?: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
interface ToolCall {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
arguments: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
interface ChatResponse {
|
|
66
|
+
content: string;
|
|
67
|
+
toolCalls?: ToolCall[];
|
|
68
|
+
finishReason: 'stop' | 'tool_calls' | 'length' | 'error';
|
|
69
|
+
usage?: TokenUsage;
|
|
70
|
+
/** Raw response from the provider for advanced use cases. */
|
|
71
|
+
raw?: unknown;
|
|
72
|
+
}
|
|
73
|
+
interface TokenUsage {
|
|
74
|
+
promptTokens: number;
|
|
75
|
+
completionTokens: number;
|
|
76
|
+
totalTokens: number;
|
|
77
|
+
}
|
|
78
|
+
interface ChatChunk {
|
|
79
|
+
/** Incremental text delta. */
|
|
80
|
+
delta: string;
|
|
81
|
+
/** Incremental tool call delta (partial). */
|
|
82
|
+
toolCallDelta?: Partial<ToolCall> & {
|
|
83
|
+
index?: number;
|
|
84
|
+
};
|
|
85
|
+
/** Set on the final chunk. */
|
|
86
|
+
finishReason?: ChatResponse['finishReason'];
|
|
87
|
+
/** Set on the final chunk. */
|
|
88
|
+
usage?: TokenUsage;
|
|
89
|
+
}
|
|
90
|
+
interface EmbeddingRequest {
|
|
91
|
+
input: string | string[];
|
|
92
|
+
model?: string;
|
|
93
|
+
/** Adapter-specific options. */
|
|
94
|
+
extra?: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
interface EmbeddingResponse {
|
|
97
|
+
embeddings: number[][];
|
|
98
|
+
usage?: {
|
|
99
|
+
totalTokens: number;
|
|
100
|
+
};
|
|
101
|
+
raw?: unknown;
|
|
102
|
+
}
|
|
103
|
+
interface AIAdapter {
|
|
104
|
+
/** Send a chat completion request. */
|
|
105
|
+
chat(request: ChatRequest): Promise<ChatResponse>;
|
|
106
|
+
/** Stream a chat completion. Adapters may omit this (falls back to chat). */
|
|
107
|
+
stream?(request: ChatRequest): AsyncIterable<ChatChunk>;
|
|
108
|
+
/** Generate embeddings. Optional capability. */
|
|
109
|
+
embeddings?(request: EmbeddingRequest): Promise<EmbeddingResponse>;
|
|
110
|
+
}
|
|
111
|
+
interface OpenAIConfig {
|
|
112
|
+
apiKey: string;
|
|
113
|
+
baseURL?: string;
|
|
114
|
+
organization?: string;
|
|
115
|
+
defaultModel?: string;
|
|
116
|
+
}
|
|
117
|
+
interface AnthropicConfig {
|
|
118
|
+
apiKey: string;
|
|
119
|
+
baseURL?: string;
|
|
120
|
+
defaultModel?: string;
|
|
121
|
+
}
|
|
122
|
+
interface GoogleConfig {
|
|
123
|
+
apiKey: string;
|
|
124
|
+
defaultModel?: string;
|
|
125
|
+
}
|
|
126
|
+
interface OllamaConfig {
|
|
127
|
+
baseURL?: string;
|
|
128
|
+
defaultModel?: string;
|
|
129
|
+
}
|
|
130
|
+
interface AIHooks {
|
|
131
|
+
/** Called before every chat request. Can modify the request. */
|
|
132
|
+
onBeforeChat?: (request: ChatRequest) => ChatRequest | Promise<ChatRequest>;
|
|
133
|
+
/** Called after every chat response. Can modify the response. */
|
|
134
|
+
onAfterChat?: (response: ChatResponse, request: ChatRequest) => ChatResponse | Promise<ChatResponse>;
|
|
135
|
+
/**
|
|
136
|
+
* Called on any adapter error.
|
|
137
|
+
*
|
|
138
|
+
* `context.request` carries the full prompt (and any provider error may
|
|
139
|
+
* originate from the underlying SDK and reference request metadata), so do
|
|
140
|
+
* not forward these verbatim to an untrusted or shared log sink — redact or
|
|
141
|
+
* summarise first.
|
|
142
|
+
*/
|
|
143
|
+
onError?: (error: Error, context: {
|
|
144
|
+
method: string;
|
|
145
|
+
request?: ChatRequest;
|
|
146
|
+
}) => void;
|
|
147
|
+
}
|
|
148
|
+
interface RetryConfig {
|
|
149
|
+
/** Max number of retries (default: 0 = no retry). */
|
|
150
|
+
maxRetries?: number;
|
|
151
|
+
/** Base delay in ms (default: 1000). Doubles on each attempt (exponential backoff). */
|
|
152
|
+
baseDelay?: number;
|
|
153
|
+
/** Whether to retry on this error. Default: retries on network errors and 429/500+ status codes. */
|
|
154
|
+
shouldRetry?: (error: Error) => boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type { AIAdapter as A, ChatChunk as C, EmbeddingRequest as E, GoogleConfig as G, ImageContent as I, JsonSchema as J, MessageContent as M, OllamaConfig as O, RetryConfig as R, TextContent as T, AIHooks as a, AnthropicConfig as b, ChatMessage as c, ChatRequest as d, ChatResponse as e, EmbeddingResponse as f, MessageRole as g, OpenAIConfig as h, TokenUsage as i, ToolCall as j, ToolCallContent as k, ToolDefinition as l, ToolResultContent as m };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
2
|
+
interface TextContent {
|
|
3
|
+
type: 'text';
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
interface ImageContent {
|
|
7
|
+
type: 'image';
|
|
8
|
+
/** Base64-encoded image data or a URL. */
|
|
9
|
+
source: string;
|
|
10
|
+
mediaType?: string;
|
|
11
|
+
}
|
|
12
|
+
interface ToolCallContent {
|
|
13
|
+
type: 'tool_call';
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
arguments: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
interface ToolResultContent {
|
|
19
|
+
type: 'tool_result';
|
|
20
|
+
id: string;
|
|
21
|
+
content: string;
|
|
22
|
+
isError?: boolean;
|
|
23
|
+
}
|
|
24
|
+
type MessageContent = string | TextContent | ImageContent | ToolCallContent | ToolResultContent;
|
|
25
|
+
interface ChatMessage {
|
|
26
|
+
role: MessageRole;
|
|
27
|
+
content: MessageContent | MessageContent[];
|
|
28
|
+
name?: string;
|
|
29
|
+
}
|
|
30
|
+
interface ToolDefinition {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
parameters: JsonSchema;
|
|
34
|
+
}
|
|
35
|
+
interface JsonSchema {
|
|
36
|
+
type: string;
|
|
37
|
+
properties?: Record<string, JsonSchema & {
|
|
38
|
+
description?: string;
|
|
39
|
+
}>;
|
|
40
|
+
required?: string[];
|
|
41
|
+
items?: JsonSchema;
|
|
42
|
+
enum?: unknown[];
|
|
43
|
+
description?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
interface ChatRequest {
|
|
47
|
+
messages: ChatMessage[];
|
|
48
|
+
model?: string;
|
|
49
|
+
temperature?: number;
|
|
50
|
+
maxTokens?: number;
|
|
51
|
+
topP?: number;
|
|
52
|
+
stop?: string[];
|
|
53
|
+
tools?: ToolDefinition[];
|
|
54
|
+
toolChoice?: 'auto' | 'none' | 'required' | {
|
|
55
|
+
name: string;
|
|
56
|
+
};
|
|
57
|
+
/** Adapter-specific options passed through untouched. */
|
|
58
|
+
extra?: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
interface ToolCall {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
arguments: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
interface ChatResponse {
|
|
66
|
+
content: string;
|
|
67
|
+
toolCalls?: ToolCall[];
|
|
68
|
+
finishReason: 'stop' | 'tool_calls' | 'length' | 'error';
|
|
69
|
+
usage?: TokenUsage;
|
|
70
|
+
/** Raw response from the provider for advanced use cases. */
|
|
71
|
+
raw?: unknown;
|
|
72
|
+
}
|
|
73
|
+
interface TokenUsage {
|
|
74
|
+
promptTokens: number;
|
|
75
|
+
completionTokens: number;
|
|
76
|
+
totalTokens: number;
|
|
77
|
+
}
|
|
78
|
+
interface ChatChunk {
|
|
79
|
+
/** Incremental text delta. */
|
|
80
|
+
delta: string;
|
|
81
|
+
/** Incremental tool call delta (partial). */
|
|
82
|
+
toolCallDelta?: Partial<ToolCall> & {
|
|
83
|
+
index?: number;
|
|
84
|
+
};
|
|
85
|
+
/** Set on the final chunk. */
|
|
86
|
+
finishReason?: ChatResponse['finishReason'];
|
|
87
|
+
/** Set on the final chunk. */
|
|
88
|
+
usage?: TokenUsage;
|
|
89
|
+
}
|
|
90
|
+
interface EmbeddingRequest {
|
|
91
|
+
input: string | string[];
|
|
92
|
+
model?: string;
|
|
93
|
+
/** Adapter-specific options. */
|
|
94
|
+
extra?: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
interface EmbeddingResponse {
|
|
97
|
+
embeddings: number[][];
|
|
98
|
+
usage?: {
|
|
99
|
+
totalTokens: number;
|
|
100
|
+
};
|
|
101
|
+
raw?: unknown;
|
|
102
|
+
}
|
|
103
|
+
interface AIAdapter {
|
|
104
|
+
/** Send a chat completion request. */
|
|
105
|
+
chat(request: ChatRequest): Promise<ChatResponse>;
|
|
106
|
+
/** Stream a chat completion. Adapters may omit this (falls back to chat). */
|
|
107
|
+
stream?(request: ChatRequest): AsyncIterable<ChatChunk>;
|
|
108
|
+
/** Generate embeddings. Optional capability. */
|
|
109
|
+
embeddings?(request: EmbeddingRequest): Promise<EmbeddingResponse>;
|
|
110
|
+
}
|
|
111
|
+
interface OpenAIConfig {
|
|
112
|
+
apiKey: string;
|
|
113
|
+
baseURL?: string;
|
|
114
|
+
organization?: string;
|
|
115
|
+
defaultModel?: string;
|
|
116
|
+
}
|
|
117
|
+
interface AnthropicConfig {
|
|
118
|
+
apiKey: string;
|
|
119
|
+
baseURL?: string;
|
|
120
|
+
defaultModel?: string;
|
|
121
|
+
}
|
|
122
|
+
interface GoogleConfig {
|
|
123
|
+
apiKey: string;
|
|
124
|
+
defaultModel?: string;
|
|
125
|
+
}
|
|
126
|
+
interface OllamaConfig {
|
|
127
|
+
baseURL?: string;
|
|
128
|
+
defaultModel?: string;
|
|
129
|
+
}
|
|
130
|
+
interface AIHooks {
|
|
131
|
+
/** Called before every chat request. Can modify the request. */
|
|
132
|
+
onBeforeChat?: (request: ChatRequest) => ChatRequest | Promise<ChatRequest>;
|
|
133
|
+
/** Called after every chat response. Can modify the response. */
|
|
134
|
+
onAfterChat?: (response: ChatResponse, request: ChatRequest) => ChatResponse | Promise<ChatResponse>;
|
|
135
|
+
/**
|
|
136
|
+
* Called on any adapter error.
|
|
137
|
+
*
|
|
138
|
+
* `context.request` carries the full prompt (and any provider error may
|
|
139
|
+
* originate from the underlying SDK and reference request metadata), so do
|
|
140
|
+
* not forward these verbatim to an untrusted or shared log sink — redact or
|
|
141
|
+
* summarise first.
|
|
142
|
+
*/
|
|
143
|
+
onError?: (error: Error, context: {
|
|
144
|
+
method: string;
|
|
145
|
+
request?: ChatRequest;
|
|
146
|
+
}) => void;
|
|
147
|
+
}
|
|
148
|
+
interface RetryConfig {
|
|
149
|
+
/** Max number of retries (default: 0 = no retry). */
|
|
150
|
+
maxRetries?: number;
|
|
151
|
+
/** Base delay in ms (default: 1000). Doubles on each attempt (exponential backoff). */
|
|
152
|
+
baseDelay?: number;
|
|
153
|
+
/** Whether to retry on this error. Default: retries on network errors and 429/500+ status codes. */
|
|
154
|
+
shouldRetry?: (error: Error) => boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type { AIAdapter as A, ChatChunk as C, EmbeddingRequest as E, GoogleConfig as G, ImageContent as I, JsonSchema as J, MessageContent as M, OllamaConfig as O, RetryConfig as R, TextContent as T, AIHooks as a, AnthropicConfig as b, ChatMessage as c, ChatRequest as d, ChatResponse as e, EmbeddingResponse as f, MessageRole as g, OpenAIConfig as h, TokenUsage as i, ToolCall as j, ToolCallContent as k, ToolDefinition as l, ToolResultContent as m };
|