@startsimpli/llm 0.4.13 → 0.4.14
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/package.json +1 -1
- package/src/__tests__/agent.test.ts +74 -1
- package/src/agent/agent.ts +68 -0
- package/src/index.ts +4 -0
- package/src/providers/interface.ts +21 -1
- package/src/providers/openai.ts +162 -1
- package/src/types.ts +38 -0
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
6
6
|
import { runAgentTurn, buildToolRegistry } from '../agent/agent';
|
|
7
7
|
import type { ToolProvider } from '../agent/types';
|
|
8
8
|
import type { ILLMProvider } from '../providers/interface';
|
|
9
|
-
import type { LLMResponse } from '../types';
|
|
9
|
+
import type { LLMResponse, NativeToolResponse } from '../types';
|
|
10
10
|
|
|
11
11
|
/** Returns the queued responses in order; repeats the last once exhausted. */
|
|
12
12
|
class QueuedProvider implements ILLMProvider {
|
|
@@ -111,3 +111,76 @@ describe('runAgentTurn', () => {
|
|
|
111
111
|
expect(() => buildToolRegistry([a, b])).toThrow(/duplicate tool name 'dup'/);
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
/** Provider that supports NATIVE tool-calling: returns queued {content, toolCalls}. */
|
|
116
|
+
class NativeQueuedProvider implements ILLMProvider {
|
|
117
|
+
readonly name = 'native';
|
|
118
|
+
readonly availableModels = ['n'] as const;
|
|
119
|
+
readonly defaultModel = 'n';
|
|
120
|
+
private i = 0;
|
|
121
|
+
constructor(private readonly turns: NativeToolResponse[]) {}
|
|
122
|
+
async generate(): Promise<LLMResponse> {
|
|
123
|
+
return { content: '', usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, model: 'n' };
|
|
124
|
+
}
|
|
125
|
+
async generateWithTools(): Promise<NativeToolResponse> {
|
|
126
|
+
const t = this.turns[Math.min(this.i, this.turns.length - 1)];
|
|
127
|
+
this.i += 1;
|
|
128
|
+
return t;
|
|
129
|
+
}
|
|
130
|
+
isAvailable() {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
describe('runAgentTurn (native tool-calling)', () => {
|
|
136
|
+
it('uses the native loop, dispatches the structured call, returns plain-text final', async () => {
|
|
137
|
+
const handler = vi.fn(async (input: { types: string[] }) => ({ saved: input.types.length }));
|
|
138
|
+
const provider = new NativeQueuedProvider([
|
|
139
|
+
{ content: null, toolCalls: [{ id: 'c1', name: 'save_schema', arguments: { types: ['Creator', 'Campaign'] } }] },
|
|
140
|
+
{ content: 'All set — I created Creator and Campaign.', toolCalls: [] },
|
|
141
|
+
]);
|
|
142
|
+
const foundry: ToolProvider = {
|
|
143
|
+
name: 'foundry',
|
|
144
|
+
tools: [{ name: 'save_schema', description: 'persist', inputSchema: { type: 'object' }, handler }],
|
|
145
|
+
};
|
|
146
|
+
const res = await runAgentTurn({
|
|
147
|
+
provider,
|
|
148
|
+
toolProviders: [foundry],
|
|
149
|
+
messages: msgs('track creators and campaigns'),
|
|
150
|
+
context: { tenant: 'x' },
|
|
151
|
+
});
|
|
152
|
+
expect(handler).toHaveBeenCalledWith({ types: ['Creator', 'Campaign'] }, { tenant: 'x' });
|
|
153
|
+
expect(res.toolCalls).toEqual([
|
|
154
|
+
{ tool: 'save_schema', input: { types: ['Creator', 'Campaign'] }, output: { saved: 2 } },
|
|
155
|
+
]);
|
|
156
|
+
// The final is the model's REAL text — no JSON echo, no synthesized fallback needed.
|
|
157
|
+
expect(res.finalText).toBe('All set — I created Creator and Campaign.');
|
|
158
|
+
expect(res.steps).toBe(2);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('does NOT retry a satisfied call (unlike the forced-json string protocol)', async () => {
|
|
162
|
+
const handler = vi.fn(async () => ({ ok: true }));
|
|
163
|
+
const provider = new NativeQueuedProvider([
|
|
164
|
+
{ content: null, toolCalls: [{ id: 'c1', name: 'save', arguments: {} }] },
|
|
165
|
+
{ content: 'Done.', toolCalls: [] },
|
|
166
|
+
]);
|
|
167
|
+
const p: ToolProvider = { name: 'p', tools: [{ name: 'save', description: 'd', handler }] };
|
|
168
|
+
const res = await runAgentTurn({ provider, toolProviders: [p], messages: msgs('go') });
|
|
169
|
+
expect(handler).toHaveBeenCalledTimes(1); // one clean call, not N retries
|
|
170
|
+
expect(res.finalText).toBe('Done.');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('captures a handler error in the native loop', async () => {
|
|
174
|
+
const provider = new NativeQueuedProvider([
|
|
175
|
+
{ content: null, toolCalls: [{ id: 'c1', name: 'boom', arguments: {} }] },
|
|
176
|
+
{ content: 'handled', toolCalls: [] },
|
|
177
|
+
]);
|
|
178
|
+
const p: ToolProvider = {
|
|
179
|
+
name: 'p',
|
|
180
|
+
tools: [{ name: 'boom', description: 'd', handler: async () => { throw new Error('db down'); } }],
|
|
181
|
+
};
|
|
182
|
+
const res = await runAgentTurn({ provider, toolProviders: [p], messages: msgs('go') });
|
|
183
|
+
expect(res.toolCalls[0].error).toBe('db down');
|
|
184
|
+
expect(res.finalText).toBe('handled');
|
|
185
|
+
});
|
|
186
|
+
});
|
package/src/agent/agent.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* changing the ToolProvider API.
|
|
11
11
|
*/
|
|
12
12
|
import { extractJson } from '../utils/json-extraction';
|
|
13
|
+
import type { NativeChatMessage, NativeToolSpec } from '../types';
|
|
13
14
|
import type {
|
|
14
15
|
AgentMessage,
|
|
15
16
|
AgentToolCall,
|
|
@@ -75,9 +76,76 @@ function parseToolCall(content: string): ParsedToolCall | null {
|
|
|
75
76
|
return { tool: obj.tool, input: 'input' in obj ? obj.input : {} };
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/** Native (structured) tool loop for providers that implement generateWithTools.
|
|
80
|
+
* Tool calls come back structured (no JSON-in-text parsing) and finals are real
|
|
81
|
+
* text, so there's no ambiguity and the model doesn't re-emit a satisfied call. */
|
|
82
|
+
async function runNativeToolLoop(
|
|
83
|
+
opts: RunAgentOptions,
|
|
84
|
+
registry: Map<string, ToolDefinition>,
|
|
85
|
+
): Promise<AgentTurnResult> {
|
|
86
|
+
const { provider, context = {}, maxSteps = 6, generateOptions } = opts;
|
|
87
|
+
const system = opts.systemPrompt?.trim() ?? '';
|
|
88
|
+
const toolSpecs: NativeToolSpec[] = [...registry.values()].map((t) => ({
|
|
89
|
+
name: t.name,
|
|
90
|
+
description: t.description,
|
|
91
|
+
parameters: t.inputSchema ?? { type: 'object', properties: {} },
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
const messages: NativeChatMessage[] = opts.messages.map((m) => ({
|
|
95
|
+
role: m.role === 'tool' ? 'tool' : m.role,
|
|
96
|
+
content: m.content,
|
|
97
|
+
}));
|
|
98
|
+
const toolCalls: AgentToolCall[] = [];
|
|
99
|
+
const toAgent = (): AgentMessage[] =>
|
|
100
|
+
messages.map((m) => ({ role: m.role, content: m.content ?? '' }));
|
|
101
|
+
|
|
102
|
+
for (let step = 1; step <= maxSteps; step++) {
|
|
103
|
+
const resp = await provider.generateWithTools!(messages, system, toolSpecs, generateOptions);
|
|
104
|
+
if (!resp.toolCalls.length) {
|
|
105
|
+
messages.push({ role: 'assistant', content: resp.content ?? '' });
|
|
106
|
+
return { finalText: resp.content ?? '', toolCalls, messages: toAgent(), steps: step, stoppedReason: 'final' };
|
|
107
|
+
}
|
|
108
|
+
messages.push({ role: 'assistant', content: resp.content, toolCalls: resp.toolCalls });
|
|
109
|
+
for (const call of resp.toolCalls) {
|
|
110
|
+
const record: AgentToolCall = { tool: call.name, input: call.arguments };
|
|
111
|
+
const tool = registry.get(call.name);
|
|
112
|
+
if (!tool) {
|
|
113
|
+
record.error = `unknown tool '${call.name}'`;
|
|
114
|
+
messages.push({ role: 'tool', toolCallId: call.id, content: `ERROR: ${record.error}` });
|
|
115
|
+
} else {
|
|
116
|
+
try {
|
|
117
|
+
const output = await tool.handler(call.arguments, context);
|
|
118
|
+
record.output = output;
|
|
119
|
+
messages.push({ role: 'tool', toolCallId: call.id, content: JSON.stringify(output ?? null) });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
122
|
+
messages.push({ role: 'tool', toolCallId: call.id, content: `ERROR: ${record.error}` });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
toolCalls.push(record);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Budget exhausted — ask for a plain-text wrap-up (tool_choice stays 'auto').
|
|
130
|
+
const resp = await provider.generateWithTools!(
|
|
131
|
+
messages,
|
|
132
|
+
`${system}\n\nYou have reached the tool-call limit. Reply to the user now in plain text.`,
|
|
133
|
+
toolSpecs,
|
|
134
|
+
generateOptions,
|
|
135
|
+
);
|
|
136
|
+
messages.push({ role: 'assistant', content: resp.content ?? '' });
|
|
137
|
+
return { finalText: resp.content ?? '', toolCalls, messages: toAgent(), steps: maxSteps, stoppedReason: 'max_steps' };
|
|
138
|
+
}
|
|
139
|
+
|
|
78
140
|
export async function runAgentTurn(opts: RunAgentOptions): Promise<AgentTurnResult> {
|
|
79
141
|
const { provider, toolProviders, context = {}, maxSteps = 6, generateOptions } = opts;
|
|
80
142
|
const registry = buildToolRegistry(toolProviders);
|
|
143
|
+
|
|
144
|
+
// Prefer native structured tool-calling when the provider supports it.
|
|
145
|
+
if (typeof provider.generateWithTools === 'function' && registry.size > 0) {
|
|
146
|
+
return runNativeToolLoop(opts, registry);
|
|
147
|
+
}
|
|
148
|
+
|
|
81
149
|
const protocol = toolProtocolPrompt(registry);
|
|
82
150
|
const baseSystem = [opts.systemPrompt?.trim(), protocol].filter(Boolean).join('\n\n');
|
|
83
151
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
LLMResponse,
|
|
3
|
+
GenerateOptions,
|
|
4
|
+
NativeToolSpec,
|
|
5
|
+
NativeChatMessage,
|
|
6
|
+
NativeToolResponse,
|
|
7
|
+
} from '../types';
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Contract that every LLM provider must implement.
|
|
@@ -29,6 +35,20 @@ export interface ILLMProvider {
|
|
|
29
35
|
options?: GenerateOptions
|
|
30
36
|
): Promise<LLMResponse>;
|
|
31
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Optional NATIVE (structured) tool-calling. Providers backed by a real tools
|
|
40
|
+
* API (OpenAI functions, Anthropic tool blocks) implement this so the agent
|
|
41
|
+
* harness can use structured tool calls + plain-text finals instead of the
|
|
42
|
+
* fragile JSON-in-text protocol. If omitted, the harness falls back to the
|
|
43
|
+
* string protocol — so existing providers (Mock, etc.) keep working unchanged.
|
|
44
|
+
*/
|
|
45
|
+
generateWithTools?(
|
|
46
|
+
messages: NativeChatMessage[],
|
|
47
|
+
systemPrompt: string,
|
|
48
|
+
tools: NativeToolSpec[],
|
|
49
|
+
options?: GenerateOptions
|
|
50
|
+
): Promise<NativeToolResponse>;
|
|
51
|
+
|
|
32
52
|
/** Check if the provider is properly configured and available */
|
|
33
53
|
isAvailable(): boolean;
|
|
34
54
|
|
package/src/providers/openai.ts
CHANGED
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ILLMProvider } from './interface';
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
LLMResponse,
|
|
15
|
+
GenerateOptions,
|
|
16
|
+
ProviderConfig,
|
|
17
|
+
NativeToolSpec,
|
|
18
|
+
NativeChatMessage,
|
|
19
|
+
NativeToolResponse,
|
|
20
|
+
} from '../types';
|
|
14
21
|
import { LLM_DEFAULTS } from '../types';
|
|
15
22
|
import { LLMProviderError } from '../errors';
|
|
16
23
|
import type { LLMErrorCode } from '../errors';
|
|
@@ -54,6 +61,30 @@ interface OpenAIErrorResponse {
|
|
|
54
61
|
};
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
interface OpenAIToolCall {
|
|
65
|
+
id: string;
|
|
66
|
+
type: 'function';
|
|
67
|
+
function: { name: string; arguments: string };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface OpenAIToolMessage {
|
|
71
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
72
|
+
content: string | null;
|
|
73
|
+
tool_calls?: OpenAIToolCall[];
|
|
74
|
+
tool_call_id?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface OpenAIToolsResponse {
|
|
78
|
+
id: string;
|
|
79
|
+
model: string;
|
|
80
|
+
choices: Array<{
|
|
81
|
+
index: number;
|
|
82
|
+
message: { role: string; content: string | null; tool_calls?: OpenAIToolCall[] };
|
|
83
|
+
finish_reason: string;
|
|
84
|
+
}>;
|
|
85
|
+
usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
const OPENAI_MODELS = [
|
|
58
89
|
'gpt-5.2',
|
|
59
90
|
'gpt-5.1',
|
|
@@ -210,6 +241,136 @@ export class OpenAIProvider implements ILLMProvider {
|
|
|
210
241
|
}
|
|
211
242
|
}
|
|
212
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Native tool-calling via OpenAI's `tools` API. Unlike generate(), this does
|
|
246
|
+
* NOT force response_format=json_object — so the model returns structured
|
|
247
|
+
* tool_calls when calling a tool and plain text when it's done, which the agent
|
|
248
|
+
* harness can tell apart unambiguously (no JSON-in-text parsing).
|
|
249
|
+
*/
|
|
250
|
+
async generateWithTools(
|
|
251
|
+
messages: NativeChatMessage[],
|
|
252
|
+
systemPrompt: string,
|
|
253
|
+
tools: NativeToolSpec[],
|
|
254
|
+
options?: GenerateOptions
|
|
255
|
+
): Promise<NativeToolResponse> {
|
|
256
|
+
if (!this.isAvailable()) {
|
|
257
|
+
throw new LLMProviderError(
|
|
258
|
+
'OpenAI API key not configured',
|
|
259
|
+
'AUTHENTICATION_ERROR',
|
|
260
|
+
false,
|
|
261
|
+
undefined,
|
|
262
|
+
this.name
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const model = options?.model ?? this.config.defaultModel;
|
|
267
|
+
const isBaseGpt5 = model === 'gpt-5' || model === 'gpt-5.1' || model === 'gpt-5.2';
|
|
268
|
+
const isFineTuned = model.startsWith('ft:');
|
|
269
|
+
const temperature = isBaseGpt5 && !isFineTuned
|
|
270
|
+
? 1.0
|
|
271
|
+
: (options?.temperature ?? this.config.defaultTemperature);
|
|
272
|
+
const maxTokens = options?.maxTokens ?? this.config.defaultMaxTokens;
|
|
273
|
+
const timeout = options?.timeout ?? this.config.timeoutMs;
|
|
274
|
+
|
|
275
|
+
const oaiMessages: OpenAIToolMessage[] = [
|
|
276
|
+
{ role: 'system', content: systemPrompt },
|
|
277
|
+
...messages.map((m): OpenAIToolMessage => {
|
|
278
|
+
if (m.role === 'tool') {
|
|
279
|
+
return { role: 'tool', content: m.content ?? '', tool_call_id: m.toolCallId };
|
|
280
|
+
}
|
|
281
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
282
|
+
return {
|
|
283
|
+
role: 'assistant',
|
|
284
|
+
content: m.content,
|
|
285
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
286
|
+
id: tc.id,
|
|
287
|
+
type: 'function',
|
|
288
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments ?? {}) },
|
|
289
|
+
})),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return { role: m.role, content: m.content ?? '' };
|
|
293
|
+
}),
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
const requestBody = {
|
|
297
|
+
model,
|
|
298
|
+
messages: oaiMessages,
|
|
299
|
+
temperature,
|
|
300
|
+
max_tokens: maxTokens,
|
|
301
|
+
tools: tools.map((t) => ({
|
|
302
|
+
type: 'function' as const,
|
|
303
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
304
|
+
})),
|
|
305
|
+
tool_choice: 'auto' as const,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const controller = new AbortController();
|
|
309
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
310
|
+
try {
|
|
311
|
+
const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
312
|
+
method: 'POST',
|
|
313
|
+
headers: {
|
|
314
|
+
'Content-Type': 'application/json',
|
|
315
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
316
|
+
},
|
|
317
|
+
body: JSON.stringify(requestBody),
|
|
318
|
+
signal: controller.signal,
|
|
319
|
+
});
|
|
320
|
+
clearTimeout(timeoutId);
|
|
321
|
+
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
const errorData = (await response.json().catch(() => ({
|
|
324
|
+
error: { message: 'Unknown error' },
|
|
325
|
+
}))) as OpenAIErrorResponse;
|
|
326
|
+
throw this.handleApiError(response.status, errorData);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const data = (await response.json()) as OpenAIToolsResponse;
|
|
330
|
+
const choice = data.choices?.[0];
|
|
331
|
+
const msg = choice?.message;
|
|
332
|
+
const toolCalls = (msg?.tool_calls ?? []).map((tc) => {
|
|
333
|
+
let args: unknown = {};
|
|
334
|
+
try {
|
|
335
|
+
args = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
336
|
+
} catch {
|
|
337
|
+
args = { _raw: tc.function.arguments };
|
|
338
|
+
}
|
|
339
|
+
return { id: tc.id, name: tc.function.name, arguments: args };
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
content: msg?.content ?? null,
|
|
343
|
+
toolCalls,
|
|
344
|
+
usage: {
|
|
345
|
+
promptTokens: data.usage?.prompt_tokens ?? 0,
|
|
346
|
+
completionTokens: data.usage?.completion_tokens ?? 0,
|
|
347
|
+
totalTokens: data.usage?.total_tokens ?? 0,
|
|
348
|
+
},
|
|
349
|
+
model: data.model,
|
|
350
|
+
finishReason: choice?.finish_reason,
|
|
351
|
+
};
|
|
352
|
+
} catch (error) {
|
|
353
|
+
clearTimeout(timeoutId);
|
|
354
|
+
if (error instanceof LLMProviderError) throw error;
|
|
355
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
356
|
+
throw new LLMProviderError(
|
|
357
|
+
`Request timed out after ${timeout}ms`,
|
|
358
|
+
'TIMEOUT',
|
|
359
|
+
true,
|
|
360
|
+
undefined,
|
|
361
|
+
this.name
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
throw new LLMProviderError(
|
|
365
|
+
`OpenAI tools request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
366
|
+
'UNKNOWN_ERROR',
|
|
367
|
+
true,
|
|
368
|
+
undefined,
|
|
369
|
+
this.name
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
213
374
|
estimateCost(
|
|
214
375
|
promptTokens: number,
|
|
215
376
|
completionTokens: number,
|
package/src/types.ts
CHANGED
|
@@ -41,6 +41,44 @@ export interface GenerateOptions {
|
|
|
41
41
|
extra?: Record<string, unknown>;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Native (structured) tool-calling types. Providers that support a real tools API
|
|
46
|
+
* (OpenAI functions, Anthropic tool blocks) implement ILLMProvider.generateWithTools
|
|
47
|
+
* so the agent harness can use structured calls instead of the JSON-in-text protocol.
|
|
48
|
+
*/
|
|
49
|
+
export interface NativeToolSpec {
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
/** JSON Schema for the tool's arguments. */
|
|
53
|
+
parameters: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A structured tool call the model emitted (arguments already parsed from JSON). */
|
|
57
|
+
export interface NativeToolCall {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
arguments: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One message in a native tool-calling conversation. */
|
|
64
|
+
export interface NativeChatMessage {
|
|
65
|
+
role: 'user' | 'assistant' | 'tool';
|
|
66
|
+
content: string | null;
|
|
67
|
+
/** Set on assistant turns that called tools. */
|
|
68
|
+
toolCalls?: NativeToolCall[];
|
|
69
|
+
/** Set on tool-result turns: which call this answers. */
|
|
70
|
+
toolCallId?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Result of one native tool-calling round-trip. */
|
|
74
|
+
export interface NativeToolResponse {
|
|
75
|
+
content: string | null;
|
|
76
|
+
toolCalls: NativeToolCall[];
|
|
77
|
+
usage?: TokenUsage;
|
|
78
|
+
model?: string;
|
|
79
|
+
finishReason?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
44
82
|
/**
|
|
45
83
|
* Provider configuration — everything needed to construct a provider instance.
|
|
46
84
|
*/
|