@sdods/agents 0.2.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.
Files changed (41) hide show
  1. package/LICENSE +17 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/adapter/claude-code.d.ts +29 -0
  4. package/dist/adapter/claude-code.js +195 -0
  5. package/dist/adapter/claude.d.ts +42 -0
  6. package/dist/adapter/claude.js +216 -0
  7. package/dist/adapter/cli-common.d.ts +46 -0
  8. package/dist/adapter/cli-common.js +145 -0
  9. package/dist/adapter/codex.d.ts +31 -0
  10. package/dist/adapter/codex.js +252 -0
  11. package/dist/adapter/factory.d.ts +41 -0
  12. package/dist/adapter/factory.js +102 -0
  13. package/dist/adapter/fake.d.ts +39 -0
  14. package/dist/adapter/fake.js +91 -0
  15. package/dist/adapter/index.d.ts +11 -0
  16. package/dist/adapter/index.js +11 -0
  17. package/dist/adapter/json-schema.d.ts +11 -0
  18. package/dist/adapter/json-schema.js +17 -0
  19. package/dist/adapter/mcp-bridge.d.ts +35 -0
  20. package/dist/adapter/mcp-bridge.js +122 -0
  21. package/dist/adapter/ollama.d.ts +62 -0
  22. package/dist/adapter/ollama.js +272 -0
  23. package/dist/adapter/openai-compat.d.ts +44 -0
  24. package/dist/adapter/openai-compat.js +200 -0
  25. package/dist/adapter/profile.d.ts +54 -0
  26. package/dist/adapter/profile.js +42 -0
  27. package/dist/adapter/tool-loop.d.ts +64 -0
  28. package/dist/adapter/tool-loop.js +192 -0
  29. package/dist/adapter/types.d.ts +109 -0
  30. package/dist/adapter/types.js +20 -0
  31. package/dist/claude-code/install.d.ts +36 -0
  32. package/dist/claude-code/install.js +183 -0
  33. package/dist/index.d.ts +5 -0
  34. package/dist/index.js +5 -0
  35. package/dist/jobs/runner.d.ts +80 -0
  36. package/dist/jobs/runner.js +197 -0
  37. package/dist/roles/index.d.ts +50 -0
  38. package/dist/roles/index.js +125 -0
  39. package/dist/roles/steps.d.ts +28 -0
  40. package/dist/roles/steps.js +75 -0
  41. package/package.json +50 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * What to keep from a browser server when the menu has to be short: look, go, act, read — in the
3
+ * order a person would use them.
4
+ */
5
+ const BROWSER_ESSENTIALS = [
6
+ 'browser_snapshot',
7
+ 'browser_navigate',
8
+ 'browser_click',
9
+ 'browser_type',
10
+ 'browser_fill_form',
11
+ 'browser_find',
12
+ 'browser_wait_for',
13
+ 'browser_console_messages',
14
+ ];
15
+ /** Ranks a server's tools so a cap keeps the useful ones rather than the alphabetical ones. */
16
+ function prioritise(names, limit) {
17
+ const ranked = [...names].sort((a, b) => {
18
+ const ia = BROWSER_ESSENTIALS.indexOf(a);
19
+ const ib = BROWSER_ESSENTIALS.indexOf(b);
20
+ return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
21
+ });
22
+ return new Set(ranked.slice(0, limit));
23
+ }
24
+ /** Playwright's own MCP server, so a role that must look at the application can. */
25
+ const PLAYWRIGHT_SERVER = {
26
+ transport: 'stdio',
27
+ command: 'npx',
28
+ args: ['playwright', 'mcp', '--headless'],
29
+ };
30
+ export async function bridgeMcpServers(opts) {
31
+ const configured = { ...(opts.servers ?? {}) };
32
+ if (opts.withPlaywright && !configured.playwright)
33
+ configured.playwright = PLAYWRIGHT_SERVER;
34
+ const entries = Object.entries(configured);
35
+ if (!entries.length)
36
+ return { tools: [], close: async () => undefined };
37
+ const { Client } = await import('@modelcontextprotocol/client');
38
+ const closers = [];
39
+ const tools = [];
40
+ for (const [name, cfg] of entries) {
41
+ try {
42
+ const client = new Client({ name: 'sdods-agent', version: '0.1.0' });
43
+ const transport = await transportFor(cfg, opts.cwd);
44
+ await client.connect(transport);
45
+ closers.push(() => client.close().catch(() => undefined));
46
+ const listed = await client.listTools();
47
+ const allowed = cfg.allowedTools?.length
48
+ ? new Set(cfg.allowedTools)
49
+ : opts.maxToolsPerServer && listed.tools.length > opts.maxToolsPerServer
50
+ ? prioritise(listed.tools.map((t) => t.name), opts.maxToolsPerServer)
51
+ : undefined;
52
+ for (const tool of listed.tools) {
53
+ if (allowed && !allowed.has(tool.name))
54
+ continue;
55
+ tools.push({
56
+ name: `mcp__${name}__${tool.name}`,
57
+ description: tool.description ?? `${name} ${tool.name}`,
58
+ // The server already speaks JSON Schema; converting it to zod and back would only lose
59
+ // detail, so it is passed through and used verbatim.
60
+ inputSchema: {},
61
+ parametersJsonSchema: (tool.inputSchema ?? {
62
+ type: 'object',
63
+ properties: {},
64
+ }),
65
+ annotations: {
66
+ readOnlyHint: tool.annotations?.readOnlyHint ?? false,
67
+ destructiveHint: tool.annotations?.destructiveHint ?? false,
68
+ },
69
+ handler: async (args) => {
70
+ const result = await client.callTool({ name: tool.name, arguments: args });
71
+ const content = (result.content ?? []).map((c) => c.type === 'text'
72
+ ? { type: 'text', text: c.text }
73
+ : c.type === 'image'
74
+ ? { type: 'image', data: c.data, mimeType: c.mimeType }
75
+ : { type: 'text', text: `[${c.type}]` });
76
+ return { content, isError: Boolean(result.isError) };
77
+ },
78
+ });
79
+ }
80
+ const kept = allowed ? [...allowed].length : listed.tools.length;
81
+ opts.onEvent?.({
82
+ type: 'status',
83
+ message: `mcp: ${name} connected (${kept} of ${listed.tools.length} tool(s))`,
84
+ });
85
+ }
86
+ catch (err) {
87
+ // A server that will not start must not take the job with it: the model simply has fewer
88
+ // tools, and the reason is on the record.
89
+ opts.onEvent?.({
90
+ type: 'status',
91
+ message: `mcp: ${name} unavailable (${err.message})`,
92
+ });
93
+ }
94
+ }
95
+ return {
96
+ tools,
97
+ close: async () => {
98
+ await Promise.all(closers.map((c) => c()));
99
+ },
100
+ };
101
+ }
102
+ async function transportFor(cfg, cwd) {
103
+ if (cfg.transport === 'http' || (!cfg.command && cfg.url)) {
104
+ if (!cfg.url)
105
+ throw new Error('an http MCP server needs a url');
106
+ const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/client');
107
+ return new StreamableHTTPClientTransport(new URL(cfg.url), {
108
+ requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
109
+ });
110
+ }
111
+ if (!cfg.command)
112
+ throw new Error('a stdio MCP server needs a command');
113
+ const { StdioClientTransport } = await import('@modelcontextprotocol/client/stdio');
114
+ return new StdioClientTransport({
115
+ command: cfg.command,
116
+ args: cfg.args ?? [],
117
+ env: { ...process.env, ...(cfg.env ?? {}) },
118
+ cwd,
119
+ stderr: 'ignore',
120
+ });
121
+ }
122
+ //# sourceMappingURL=mcp-bridge.js.map
@@ -0,0 +1,62 @@
1
+ import { type CompleteRequest, type CompleteResult, type LlmAdapter, type RunAgentOptions, type RunAgentResult } from './types.js';
2
+ export interface OllamaOptions {
3
+ baseUrl?: string;
4
+ model?: string;
5
+ /** Sets `options.num_ctx`, and the ceiling the prompt is measured against. */
6
+ contextTokens?: number;
7
+ temperature?: number;
8
+ timeoutMs?: number;
9
+ /** How long the server keeps the model in memory between turns. */
10
+ keepAlive?: string;
11
+ fetchImpl?: typeof fetch;
12
+ }
13
+ export interface OllamaModel {
14
+ name: string;
15
+ sizeBytes: number;
16
+ parameterSize?: string;
17
+ family?: string;
18
+ capabilities?: string[];
19
+ contextLength?: number;
20
+ }
21
+ export declare class OllamaAdapter implements LlmAdapter {
22
+ private readonly opts;
23
+ readonly provider: "ollama";
24
+ readonly defaultModel: string;
25
+ readonly baseUrl: string;
26
+ private readonly fetchImpl;
27
+ private readonly timeoutMs;
28
+ private readonly temperature;
29
+ private readonly contextTokens?;
30
+ private readonly keepAlive;
31
+ constructor(opts?: OllamaOptions);
32
+ /** Installed models. Throws a readable error when nothing is listening. */
33
+ models(): Promise<OllamaModel[]>;
34
+ /** What a model can do, and the context its weights allow. */
35
+ show(model: string): Promise<{
36
+ capabilities: string[];
37
+ contextLength?: number;
38
+ }>;
39
+ /** The context a loaded model is actually running with — usually far below its maximum. */
40
+ loadedContext(model: string): Promise<number | undefined>;
41
+ version(): Promise<string>;
42
+ /** True when a server answers at all — used by detection and `doctor`, so it is quick. */
43
+ static reachable(baseUrl?: string, timeoutMs?: number): Promise<boolean>;
44
+ complete(req: CompleteRequest): Promise<CompleteResult>;
45
+ stream(req: CompleteRequest): AsyncIterable<string>;
46
+ runAgent(o: RunAgentOptions): Promise<RunAgentResult>;
47
+ /** Native shapes: tool calls carry no id and arguments arrive already parsed. */
48
+ private transport;
49
+ /**
50
+ * Refuses to start a job whose prompt cannot fit, instead of letting Ollama truncate it.
51
+ *
52
+ * This is the failure nobody reports, because the answer still looks plausible — so it is a
53
+ * hard error with the two commands that fix it, not a warning.
54
+ */
55
+ private assertFits;
56
+ private options;
57
+ private chat;
58
+ private get;
59
+ private post;
60
+ private request;
61
+ }
62
+ //# sourceMappingURL=ollama.d.ts.map
@@ -0,0 +1,272 @@
1
+ import { AgentsConfigError, DEFAULT_MODELS, } from './types.js';
2
+ import { readToolArgs, runToolLoop, } from './tool-loop.js';
3
+ import { toolParameters } from './json-schema.js';
4
+ import { bridgeMcpServers } from './mcp-bridge.js';
5
+ /**
6
+ * Models running on this machine, through Ollama's native API.
7
+ *
8
+ * Ollama also speaks the OpenAI protocol, and `openai-compatible` will talk to it — but that path
9
+ * cannot set `num_ctx`. Ollama loads a model at 4096 tokens by default even when the weights
10
+ * advertise 128k, and it truncates a longer prompt **silently**: the model answers confidently
11
+ * from half a question and nothing anywhere reports a problem. An agent prompt carrying tool
12
+ * schemas passes 4096 on the first turn, so the native endpoint is the only honest transport.
13
+ *
14
+ * The same endpoint buys two more things worth having: `format` accepts a JSON schema, which is
15
+ * what makes a 7B model reliable at structured output, and `/api/tags`, `/api/show` and `/api/ps`
16
+ * report what is installed, what it can do, and how much context it was actually loaded with.
17
+ */
18
+ const DEFAULT_HOST = 'http://127.0.0.1:11434';
19
+ /** Local generation is minutes, not seconds: a first load pulls weights into memory. */
20
+ const DEFAULT_TIMEOUT_MS = 300_000;
21
+ /** Rough characters-per-token for the budget guard; deliberately pessimistic. */
22
+ const CHARS_PER_TOKEN = 3.5;
23
+ export class OllamaAdapter {
24
+ opts;
25
+ provider = 'ollama';
26
+ defaultModel;
27
+ baseUrl;
28
+ fetchImpl;
29
+ timeoutMs;
30
+ temperature;
31
+ contextTokens;
32
+ keepAlive;
33
+ constructor(opts = {}) {
34
+ this.opts = opts;
35
+ this.defaultModel = opts.model ?? process.env.OLLAMA_MODEL ?? DEFAULT_MODELS.ollama;
36
+ this.baseUrl = normalizeHost(opts.baseUrl ?? process.env.OLLAMA_HOST ?? DEFAULT_HOST);
37
+ this.fetchImpl = opts.fetchImpl ?? fetch;
38
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
39
+ this.temperature = opts.temperature ?? 0;
40
+ this.contextTokens = opts.contextTokens;
41
+ this.keepAlive = opts.keepAlive ?? '10m';
42
+ }
43
+ /* ── discovery ──────────────────────────────────────────────────────────── */
44
+ /** Installed models. Throws a readable error when nothing is listening. */
45
+ async models() {
46
+ const json = (await this.get('/api/tags'));
47
+ return (json.models ?? []).map((m) => ({
48
+ name: m.name,
49
+ sizeBytes: m.size,
50
+ family: m.details?.family,
51
+ parameterSize: m.details?.parameter_size,
52
+ }));
53
+ }
54
+ /** What a model can do, and the context its weights allow. */
55
+ async show(model) {
56
+ const json = (await this.post('/api/show', { model }));
57
+ const ctxKey = Object.keys(json.model_info ?? {}).find((k) => k.endsWith('.context_length'));
58
+ return {
59
+ capabilities: json.capabilities ?? [],
60
+ contextLength: ctxKey ? Number(json.model_info[ctxKey]) : undefined,
61
+ };
62
+ }
63
+ /** The context a loaded model is actually running with — usually far below its maximum. */
64
+ async loadedContext(model) {
65
+ const json = (await this.get('/api/ps'));
66
+ const hit = (json.models ?? []).find((m) => m.name === model || m.model === model);
67
+ return hit?.context_length;
68
+ }
69
+ async version() {
70
+ const json = (await this.get('/api/version'));
71
+ return json.version ?? 'unknown';
72
+ }
73
+ /** True when a server answers at all — used by detection and `doctor`, so it is quick. */
74
+ static async reachable(baseUrl, timeoutMs = 400) {
75
+ const url = `${normalizeHost(baseUrl ?? process.env.OLLAMA_HOST ?? DEFAULT_HOST)}/api/tags`;
76
+ try {
77
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
78
+ return res.ok;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ /* ── generation ─────────────────────────────────────────────────────────── */
85
+ async complete(req) {
86
+ const model = req.model ?? this.defaultModel;
87
+ const json = (await this.chat({
88
+ model,
89
+ messages: [
90
+ ...(req.system ? [{ role: 'system', content: req.system }] : []),
91
+ { role: 'user', content: req.prompt },
92
+ ],
93
+ ...(req.responseSchema ? { format: req.responseSchema } : {}),
94
+ options: this.options(req.maxTokens),
95
+ }, req.signal));
96
+ return {
97
+ text: json.message?.content ?? '',
98
+ usage: usageOf(json),
99
+ costUsd: 0,
100
+ model,
101
+ };
102
+ }
103
+ async *stream(req) {
104
+ yield (await this.complete(req)).text;
105
+ }
106
+ async runAgent(o) {
107
+ const model = o.model ?? this.defaultModel;
108
+ const bridged = await bridgeMcpServers({
109
+ servers: o.mcpServers,
110
+ withPlaywright: o.needsBrowser,
111
+ maxToolsPerServer: o.maxBridgedTools,
112
+ cwd: o.cwd,
113
+ onEvent: o.onEvent,
114
+ });
115
+ try {
116
+ const withMcp = { ...o, tools: [...o.tools, ...bridged.tools] };
117
+ await this.assertFits(model, withMcp, o.onEvent);
118
+ return await runToolLoop(this.transport(model, withMcp), withMcp);
119
+ }
120
+ finally {
121
+ await bridged.close();
122
+ }
123
+ }
124
+ /** Native shapes: tool calls carry no id and arguments arrive already parsed. */
125
+ transport(model, o) {
126
+ return {
127
+ model,
128
+ start: (system, prompt) => [
129
+ { role: 'system', content: system },
130
+ { role: 'user', content: prompt },
131
+ ],
132
+ send: async (history, tools, signal, turnOpts) => {
133
+ // Ollama's native API has no tool_choice, so 'required' is said in words on the turn it
134
+ // applies to — which is enough to stop a small model narrating instead of calling.
135
+ const nudged = turnOpts?.toolChoice === 'required' && tools.length
136
+ ? [
137
+ ...history,
138
+ {
139
+ role: 'system',
140
+ content: 'Reply with a tool call, not prose. Call exactly one tool.',
141
+ },
142
+ ]
143
+ : history;
144
+ const json = (await this.chat({
145
+ model,
146
+ messages: nudged,
147
+ tools: tools.length ? nativeTools(tools) : undefined,
148
+ options: this.options(o.maxTokens),
149
+ }, signal));
150
+ const calls = (json.message?.tool_calls ?? []).map((call, i) => {
151
+ const { args, error } = readToolArgs(call.function?.arguments);
152
+ return { id: `call_${i}`, name: call.function?.name ?? '', args, error };
153
+ });
154
+ return {
155
+ text: json.message?.content ?? '',
156
+ calls,
157
+ usage: usageOf(json),
158
+ finishReason: json.done_reason ?? 'stop',
159
+ };
160
+ },
161
+ pushAssistant: (history, turn) => {
162
+ history.push({
163
+ role: 'assistant',
164
+ content: turn.text,
165
+ tool_calls: turn.calls.length
166
+ ? turn.calls.map((c) => ({ function: { name: c.name, arguments: c.args } }))
167
+ : undefined,
168
+ });
169
+ },
170
+ pushToolResult: (history, call, output) => {
171
+ history.push({ role: 'tool', tool_name: call.name, content: output });
172
+ },
173
+ // Electricity is not billed per token: local runs are free, and budget gates never fire.
174
+ cost: () => 0,
175
+ };
176
+ }
177
+ /**
178
+ * Refuses to start a job whose prompt cannot fit, instead of letting Ollama truncate it.
179
+ *
180
+ * This is the failure nobody reports, because the answer still looks plausible — so it is a
181
+ * hard error with the two commands that fix it, not a warning.
182
+ */
183
+ async assertFits(model, o, onEvent) {
184
+ const chars = o.system.length + o.prompt.length + JSON.stringify(nativeTools(o.tools) ?? []).length;
185
+ const estimate = Math.ceil(chars / CHARS_PER_TOKEN);
186
+ const want = this.contextTokens;
187
+ if (want && estimate < want * 0.9)
188
+ return;
189
+ const loaded = await this.loadedContext(model).catch(() => undefined);
190
+ const ceiling = want ?? loaded;
191
+ if (!ceiling)
192
+ return;
193
+ if (estimate < ceiling * 0.9)
194
+ return;
195
+ const max = (await this.show(model).catch(() => undefined))?.contextLength;
196
+ const target = Math.min(max ?? 32_768, Math.max(8192, 2 ** Math.ceil(Math.log2(estimate * 2))));
197
+ throw new AgentsConfigError(`The prompt for this job is about ${estimate.toLocaleString()} tokens, but ${model} is running with a ${ceiling.toLocaleString()}-token context. Ollama would truncate it without saying so.`, `Set agents.contextTokens: ${target} in the project yaml, or start the server with OLLAMA_CONTEXT_LENGTH=${target}${max ? ` (this model supports up to ${max.toLocaleString()})` : ''}. A smaller tool set (agents.profile: small) also shortens the prompt.`);
198
+ onEvent?.({ type: 'status', message: 'context checked' });
199
+ }
200
+ options(maxTokens) {
201
+ return {
202
+ temperature: this.temperature,
203
+ // Changing num_ctx reloads the model, so it is fixed once per adapter, from configuration.
204
+ ...(this.contextTokens ? { num_ctx: this.contextTokens } : {}),
205
+ ...(maxTokens ? { num_predict: maxTokens } : {}),
206
+ };
207
+ }
208
+ /* ── transport ──────────────────────────────────────────────────────────── */
209
+ async chat(body, signal) {
210
+ // stream defaults to true, which would return NDJSON; keep_alive stops the model being
211
+ // unloaded and reloaded between turns; think spends the output budget on reasoning tokens
212
+ // that this loop has no use for.
213
+ return this.post('/api/chat', { stream: false, keep_alive: this.keepAlive, think: false, ...body }, signal);
214
+ }
215
+ async get(path, signal) {
216
+ return this.request(path, undefined, signal);
217
+ }
218
+ async post(path, body, signal) {
219
+ return this.request(path, body, signal);
220
+ }
221
+ async request(path, body, signal) {
222
+ const url = `${this.baseUrl}${path}`;
223
+ const deadline = AbortSignal.timeout(this.timeoutMs);
224
+ const composed = signal ? AbortSignal.any([signal, deadline]) : deadline;
225
+ let res;
226
+ try {
227
+ res = await this.fetchImpl(url, {
228
+ method: body ? 'POST' : 'GET',
229
+ headers: body ? { 'content-type': 'application/json' } : undefined,
230
+ body: body ? JSON.stringify(body) : undefined,
231
+ signal: composed,
232
+ });
233
+ }
234
+ catch (err) {
235
+ if (signal?.aborted)
236
+ throw err;
237
+ if (deadline.aborted)
238
+ throw new AgentsConfigError(`${url} did not answer within ${Math.round(this.timeoutMs / 1000)}s.`, 'Raise agents.requestTimeoutMs, or check that the model is not still loading (`ollama ps`).');
239
+ throw new AgentsConfigError(`Cannot reach Ollama at ${this.baseUrl} (${err.message}).`, 'Start it with `ollama serve`, or point agents.baseUrl / OLLAMA_HOST at the machine running it.');
240
+ }
241
+ if (!res.ok) {
242
+ const text = await res.text().catch(() => '');
243
+ if (res.status === 404 && /not found/i.test(text)) {
244
+ const model = String(body?.model ?? this.defaultModel);
245
+ throw new AgentsConfigError(`Ollama has no model named "${model}".`, `Pull it with \`ollama pull ${model}\`, or pick one of the installed models with --model.`);
246
+ }
247
+ throw new Error(`${url} → ${res.status} ${text}`);
248
+ }
249
+ return res.json();
250
+ }
251
+ }
252
+ function usageOf(json) {
253
+ return { inputTokens: json.prompt_eval_count ?? 0, outputTokens: json.eval_count ?? 0 };
254
+ }
255
+ function nativeTools(tools) {
256
+ if (!tools.length)
257
+ return undefined;
258
+ return tools.map((t) => ({
259
+ type: 'function',
260
+ function: {
261
+ name: t.name,
262
+ description: t.description,
263
+ parameters: toolParameters(t.inputSchema, t.parametersJsonSchema),
264
+ },
265
+ }));
266
+ }
267
+ /** Accepts `host:port`, a full URL, or one with the OpenAI `/v1` suffix already attached. */
268
+ function normalizeHost(raw) {
269
+ const withScheme = /^https?:\/\//.test(raw) ? raw : `http://${raw}`;
270
+ return withScheme.replace(/\/+$/, '').replace(/\/v1$/, '');
271
+ }
272
+ //# sourceMappingURL=ollama.js.map
@@ -0,0 +1,44 @@
1
+ import { type CompleteRequest, type CompleteResult, type LlmAdapter, type RunAgentOptions, type RunAgentResult } from './types.js';
2
+ export interface OpenAiCompatOptions {
3
+ baseUrl?: string;
4
+ apiKey?: string;
5
+ model?: string;
6
+ fetchImpl?: typeof fetch;
7
+ /** USD per 1M tokens, for cost estimates */
8
+ prices?: Record<string, {
9
+ input: number;
10
+ output: number;
11
+ }>;
12
+ /** Per-request ceiling. Local models are minutes, not seconds, so this is generous. */
13
+ timeoutMs?: number;
14
+ /** 0 by default: test generation should be reproducible, not creative. */
15
+ temperature?: number;
16
+ }
17
+ /** Chat-completions tool loop for OpenAI-compatible endpoints (OpenAI, Azure, Ollama, vLLM …). */
18
+ export declare class OpenAiCompatibleAdapter implements LlmAdapter {
19
+ private readonly opts;
20
+ readonly provider: "openai-compatible";
21
+ readonly defaultModel: string;
22
+ private readonly baseUrl;
23
+ private readonly fetchImpl;
24
+ private readonly timeoutMs;
25
+ private readonly temperature;
26
+ constructor(opts?: OpenAiCompatOptions);
27
+ private key;
28
+ /**
29
+ * One chat-completions call, with a deadline and a single retry.
30
+ *
31
+ * The deadline matters most for local servers, which can sit silent for minutes while a model
32
+ * loads; without it a hung server hangs the job forever. The retry covers the transient half of
33
+ * the failures (connection reset, 429, 5xx) and nothing else — a 400 or a missing model is the
34
+ * caller's problem and is reported as such.
35
+ */
36
+ private chat;
37
+ complete(req: CompleteRequest): Promise<CompleteResult>;
38
+ stream(req: CompleteRequest): AsyncIterable<string>;
39
+ runAgent(o: RunAgentOptions): Promise<RunAgentResult>;
40
+ /** Chat-completions shapes: tool calls carry an id, results are `{role:'tool', tool_call_id}`. */
41
+ private transport;
42
+ private cost;
43
+ }
44
+ //# sourceMappingURL=openai-compat.d.ts.map