@germanescobar/anita 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +353 -0
- package/dist/agent/agents.d.ts +16 -0
- package/dist/agent/agents.js +115 -0
- package/dist/agent/context-budget.d.ts +7 -0
- package/dist/agent/context-budget.js +17 -0
- package/dist/agent/context-builder.d.ts +34 -0
- package/dist/agent/context-builder.js +175 -0
- package/dist/agent/executor.d.ts +13 -0
- package/dist/agent/executor.js +65 -0
- package/dist/agent/loop.d.ts +54 -0
- package/dist/agent/loop.js +548 -0
- package/dist/agent/policies.d.ts +25 -0
- package/dist/agent/policies.js +177 -0
- package/dist/agent/session.d.ts +12 -0
- package/dist/agent/session.js +42 -0
- package/dist/attachments.d.ts +3 -0
- package/dist/attachments.js +73 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +327 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/models/anthropic.d.ts +15 -0
- package/dist/models/anthropic.js +195 -0
- package/dist/models/openai-responses.d.ts +62 -0
- package/dist/models/openai-responses.js +377 -0
- package/dist/models/openai.d.ts +32 -0
- package/dist/models/openai.js +330 -0
- package/dist/models/provider.d.ts +33 -0
- package/dist/models/provider.js +1 -0
- package/dist/models/resolve.d.ts +48 -0
- package/dist/models/resolve.js +211 -0
- package/dist/security/sensitive-content.d.ts +6 -0
- package/dist/security/sensitive-content.js +59 -0
- package/dist/skills/skills.d.ts +62 -0
- package/dist/skills/skills.js +371 -0
- package/dist/storage/event-store.d.ts +7 -0
- package/dist/storage/event-store.js +36 -0
- package/dist/storage/session-store.d.ts +11 -0
- package/dist/storage/session-store.js +64 -0
- package/dist/tools/delete-file.d.ts +2 -0
- package/dist/tools/delete-file.js +25 -0
- package/dist/tools/edit-file.d.ts +2 -0
- package/dist/tools/edit-file.js +50 -0
- package/dist/tools/read-file.d.ts +2 -0
- package/dist/tools/read-file.js +122 -0
- package/dist/tools/registry.d.ts +9 -0
- package/dist/tools/registry.js +122 -0
- package/dist/tools/run-command.d.ts +2 -0
- package/dist/tools/run-command.js +103 -0
- package/dist/tools/write-file.d.ts +2 -0
- package/dist/tools/write-file.js +29 -0
- package/dist/types/agent.d.ts +44 -0
- package/dist/types/agent.js +1 -0
- package/dist/types/conversation.d.ts +43 -0
- package/dist/types/conversation.js +201 -0
- package/dist/types/events.d.ts +8 -0
- package/dist/types/events.js +1 -0
- package/dist/types/messages.d.ts +39 -0
- package/dist/types/messages.js +1 -0
- package/dist/types/output.d.ts +19 -0
- package/dist/types/output.js +1 -0
- package/dist/types/stream.d.ts +55 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/tools.d.ts +28 -0
- package/dist/types/tools.js +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { conversationItemsToMessages } from "../types/conversation.js";
|
|
3
|
+
export class AnthropicProvider {
|
|
4
|
+
client;
|
|
5
|
+
model;
|
|
6
|
+
constructor(model) {
|
|
7
|
+
this.client = new Anthropic();
|
|
8
|
+
this.model = model;
|
|
9
|
+
}
|
|
10
|
+
async chat(params) {
|
|
11
|
+
const response = await this.client.messages.create({
|
|
12
|
+
model: this.model,
|
|
13
|
+
max_tokens: 16384,
|
|
14
|
+
system: params.systemPrompt,
|
|
15
|
+
messages: conversationItemsToMessages(params.conversationItems).map((m) => this.toAnthropicMessage(m)),
|
|
16
|
+
tools: params.tools.map((t) => this.toAnthropicTool(t)),
|
|
17
|
+
}, { signal: params.signal });
|
|
18
|
+
return {
|
|
19
|
+
stopReason: this.mapStopReason(response.stop_reason),
|
|
20
|
+
content: response.content.map((block) => this.fromAnthropicBlock(block)),
|
|
21
|
+
usage: {
|
|
22
|
+
inputTokens: response.usage.input_tokens,
|
|
23
|
+
outputTokens: response.usage.output_tokens,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async *streamChat(params) {
|
|
28
|
+
const stream = await this.client.messages.create({
|
|
29
|
+
model: this.model,
|
|
30
|
+
max_tokens: 16384,
|
|
31
|
+
system: params.systemPrompt,
|
|
32
|
+
messages: conversationItemsToMessages(params.conversationItems).map((m) => this.toAnthropicMessage(m)),
|
|
33
|
+
tools: params.tools.map((t) => this.toAnthropicTool(t)),
|
|
34
|
+
stream: true,
|
|
35
|
+
}, { signal: params.signal });
|
|
36
|
+
const blocks = new Map();
|
|
37
|
+
let stopReason = "end_turn";
|
|
38
|
+
let inputTokens = 0;
|
|
39
|
+
let outputTokens = 0;
|
|
40
|
+
for await (const event of stream) {
|
|
41
|
+
switch (event.type) {
|
|
42
|
+
case "message_start":
|
|
43
|
+
inputTokens = event.message.usage.input_tokens;
|
|
44
|
+
outputTokens = event.message.usage.output_tokens;
|
|
45
|
+
break;
|
|
46
|
+
case "message_delta":
|
|
47
|
+
stopReason = this.mapStopReason(event.delta.stop_reason);
|
|
48
|
+
outputTokens = event.usage.output_tokens;
|
|
49
|
+
break;
|
|
50
|
+
case "content_block_start": {
|
|
51
|
+
const block = event.content_block;
|
|
52
|
+
if (block.type === "text") {
|
|
53
|
+
blocks.set(event.index, { type: "text", text: block.text });
|
|
54
|
+
}
|
|
55
|
+
else if (block.type === "tool_use") {
|
|
56
|
+
blocks.set(event.index, {
|
|
57
|
+
type: "tool_use",
|
|
58
|
+
id: block.id,
|
|
59
|
+
name: block.name,
|
|
60
|
+
input: block.input,
|
|
61
|
+
inputJson: "",
|
|
62
|
+
});
|
|
63
|
+
yield {
|
|
64
|
+
type: "tool_call_delta",
|
|
65
|
+
index: event.index,
|
|
66
|
+
id: block.id,
|
|
67
|
+
name: block.name,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "content_block_delta": {
|
|
73
|
+
const block = blocks.get(event.index);
|
|
74
|
+
if (event.delta.type === "text_delta" && block?.type === "text") {
|
|
75
|
+
block.text += event.delta.text;
|
|
76
|
+
yield { type: "assistant_text_delta", text: event.delta.text };
|
|
77
|
+
}
|
|
78
|
+
else if (event.delta.type === "input_json_delta" &&
|
|
79
|
+
block?.type === "tool_use") {
|
|
80
|
+
block.inputJson += event.delta.partial_json;
|
|
81
|
+
yield {
|
|
82
|
+
type: "tool_call_delta",
|
|
83
|
+
index: event.index,
|
|
84
|
+
id: block.id,
|
|
85
|
+
name: block.name,
|
|
86
|
+
inputDelta: event.delta.partial_json,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const content = [];
|
|
94
|
+
for (const [, block] of [...blocks].sort(([left], [right]) => left - right)) {
|
|
95
|
+
if (block.type === "text") {
|
|
96
|
+
content.push({ type: "text", text: block.text });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
content.push({
|
|
100
|
+
type: "tool_use",
|
|
101
|
+
id: block.id,
|
|
102
|
+
name: block.name,
|
|
103
|
+
input: this.parseToolInput(block.inputJson, block.input),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
yield {
|
|
107
|
+
type: "response",
|
|
108
|
+
response: {
|
|
109
|
+
stopReason,
|
|
110
|
+
content,
|
|
111
|
+
usage: {
|
|
112
|
+
inputTokens,
|
|
113
|
+
outputTokens,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
toAnthropicMessage(msg) {
|
|
119
|
+
if (typeof msg.content === "string") {
|
|
120
|
+
return { role: msg.role, content: msg.content };
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
role: msg.role,
|
|
124
|
+
content: msg.content.map((block) => {
|
|
125
|
+
switch (block.type) {
|
|
126
|
+
case "text":
|
|
127
|
+
return { type: "text", text: block.text };
|
|
128
|
+
case "image":
|
|
129
|
+
case "file":
|
|
130
|
+
throw new Error("AnthropicProvider does not support Anita attachment blocks yet.");
|
|
131
|
+
case "tool_use":
|
|
132
|
+
return {
|
|
133
|
+
type: "tool_use",
|
|
134
|
+
id: block.id,
|
|
135
|
+
name: block.name,
|
|
136
|
+
input: block.input,
|
|
137
|
+
};
|
|
138
|
+
case "tool_result":
|
|
139
|
+
return {
|
|
140
|
+
type: "tool_result",
|
|
141
|
+
tool_use_id: block.toolUseId,
|
|
142
|
+
content: block.content,
|
|
143
|
+
is_error: block.isError,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
toAnthropicTool(tool) {
|
|
150
|
+
return {
|
|
151
|
+
name: tool.name,
|
|
152
|
+
description: tool.description,
|
|
153
|
+
input_schema: tool.parameters,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
fromAnthropicBlock(block) {
|
|
157
|
+
switch (block.type) {
|
|
158
|
+
case "text":
|
|
159
|
+
return { type: "text", text: block.text };
|
|
160
|
+
case "tool_use":
|
|
161
|
+
return {
|
|
162
|
+
type: "tool_use",
|
|
163
|
+
id: block.id,
|
|
164
|
+
name: block.name,
|
|
165
|
+
input: block.input,
|
|
166
|
+
};
|
|
167
|
+
default:
|
|
168
|
+
return { type: "text", text: JSON.stringify(block) };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
mapStopReason(stopReason) {
|
|
172
|
+
switch (stopReason) {
|
|
173
|
+
case "tool_use":
|
|
174
|
+
return "tool_use";
|
|
175
|
+
case "max_tokens":
|
|
176
|
+
return "max_tokens";
|
|
177
|
+
default:
|
|
178
|
+
return "end_turn";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
parseToolInput(inputJson, fallback) {
|
|
182
|
+
if (!inputJson)
|
|
183
|
+
return fallback;
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(inputJson);
|
|
186
|
+
return this.isRecord(parsed) ? parsed : fallback;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return fallback;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
isRecord(value) {
|
|
193
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import type { ChatParams, ModelProvider, ModelStreamEvent } from "./provider.js";
|
|
3
|
+
import type { ModelResponse, StopReason } from "../types/agent.js";
|
|
4
|
+
import { type ConversationItem } from "../types/conversation.js";
|
|
5
|
+
import type { ContentBlock, Message } from "../types/messages.js";
|
|
6
|
+
import type { ToolSchema } from "../types/tools.js";
|
|
7
|
+
/**
|
|
8
|
+
* Provider that uses the OpenAI Responses API instead of Chat Completions.
|
|
9
|
+
*
|
|
10
|
+
* The Responses API uses a conversation-item model where inputs and outputs are
|
|
11
|
+
* represented as typed items (messages, function calls, function call outputs,
|
|
12
|
+
* reasoning items). This provider converts our internal message format into
|
|
13
|
+
* Responses API input items and converts the response output items back into
|
|
14
|
+
* our internal ContentBlock format.
|
|
15
|
+
*
|
|
16
|
+
* Key differences from Chat Completions:
|
|
17
|
+
* - The system prompt is passed as `instructions`, not a system message.
|
|
18
|
+
* - Function calls use `call_id` / `type: "function_call"`.
|
|
19
|
+
* - Function call outputs use `type: "function_call_output"`.
|
|
20
|
+
* - Reasoning summaries are explicit output items.
|
|
21
|
+
* - The overall response has a `status` field instead of `finish_reason`.
|
|
22
|
+
*/
|
|
23
|
+
export declare class OpenAIResponsesProvider implements ModelProvider {
|
|
24
|
+
private client;
|
|
25
|
+
private model;
|
|
26
|
+
constructor(model: string, options?: {
|
|
27
|
+
apiKey?: string;
|
|
28
|
+
baseURL?: string;
|
|
29
|
+
});
|
|
30
|
+
chat(params: ChatParams): Promise<ModelResponse>;
|
|
31
|
+
streamChat(params: ChatParams): AsyncIterable<ModelStreamEvent>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convert internal messages into Responses API input items.
|
|
35
|
+
*
|
|
36
|
+
* The Responses API accepts an array of input items. Our internal
|
|
37
|
+
* representation uses role-based messages with content blocks, so we map:
|
|
38
|
+
*
|
|
39
|
+
* - user text → { type: "message", role: "user", content: [...] }
|
|
40
|
+
* - assistant text → { type: "message", role: "assistant", content: [...] }
|
|
41
|
+
* - assistant tool_use → { type: "function_call", ... }
|
|
42
|
+
* - user tool_result → { type: "function_call_output", ... }
|
|
43
|
+
*/
|
|
44
|
+
export declare function messagesToInputItems(messages: Message[]): OpenAI.Responses.ResponseInputItem[];
|
|
45
|
+
export declare function conversationItemsToInputItems(conversationItems: ConversationItem[]): OpenAI.Responses.ResponseInputItem[];
|
|
46
|
+
export declare function toFunctionTool(tool: ToolSchema): OpenAI.Responses.FunctionTool;
|
|
47
|
+
/**
|
|
48
|
+
* Convert a Responses API response into our internal ModelResponse.
|
|
49
|
+
*
|
|
50
|
+
* Output items we handle:
|
|
51
|
+
* - message → text content blocks
|
|
52
|
+
* - function_call → tool_use content blocks
|
|
53
|
+
* - reasoning → reasoning string (summary text)
|
|
54
|
+
*
|
|
55
|
+
* We determine the stop reason from the response status:
|
|
56
|
+
* - completed + has function_calls → tool_use
|
|
57
|
+
* - completed → end_turn
|
|
58
|
+
* - incomplete + reason max_tokens → max_tokens
|
|
59
|
+
* - everything else → error
|
|
60
|
+
*/
|
|
61
|
+
export declare function responseToModelResponse(response: OpenAI.Responses.Response): ModelResponse;
|
|
62
|
+
export declare function mapStopReason(response: OpenAI.Responses.Response, content: ContentBlock[]): StopReason;
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
/**
|
|
3
|
+
* Provider that uses the OpenAI Responses API instead of Chat Completions.
|
|
4
|
+
*
|
|
5
|
+
* The Responses API uses a conversation-item model where inputs and outputs are
|
|
6
|
+
* represented as typed items (messages, function calls, function call outputs,
|
|
7
|
+
* reasoning items). This provider converts our internal message format into
|
|
8
|
+
* Responses API input items and converts the response output items back into
|
|
9
|
+
* our internal ContentBlock format.
|
|
10
|
+
*
|
|
11
|
+
* Key differences from Chat Completions:
|
|
12
|
+
* - The system prompt is passed as `instructions`, not a system message.
|
|
13
|
+
* - Function calls use `call_id` / `type: "function_call"`.
|
|
14
|
+
* - Function call outputs use `type: "function_call_output"`.
|
|
15
|
+
* - Reasoning summaries are explicit output items.
|
|
16
|
+
* - The overall response has a `status` field instead of `finish_reason`.
|
|
17
|
+
*/
|
|
18
|
+
export class OpenAIResponsesProvider {
|
|
19
|
+
client;
|
|
20
|
+
model;
|
|
21
|
+
constructor(model, options) {
|
|
22
|
+
this.client = new OpenAI({
|
|
23
|
+
apiKey: options?.apiKey ?? process.env.OPENAI_API_KEY ?? "not-needed",
|
|
24
|
+
baseURL: options?.baseURL,
|
|
25
|
+
});
|
|
26
|
+
this.model = model;
|
|
27
|
+
}
|
|
28
|
+
async chat(params) {
|
|
29
|
+
const inputItems = conversationItemsToInputItems(params.conversationItems);
|
|
30
|
+
const tools = params.tools.map(toFunctionTool);
|
|
31
|
+
const response = await this.client.responses.create({
|
|
32
|
+
model: this.model,
|
|
33
|
+
instructions: params.systemPrompt,
|
|
34
|
+
input: inputItems,
|
|
35
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
36
|
+
}, { signal: params.signal });
|
|
37
|
+
return responseToModelResponse(response);
|
|
38
|
+
}
|
|
39
|
+
async *streamChat(params) {
|
|
40
|
+
const inputItems = conversationItemsToInputItems(params.conversationItems);
|
|
41
|
+
const tools = params.tools.map(toFunctionTool);
|
|
42
|
+
const stream = await this.client.responses.create({
|
|
43
|
+
model: this.model,
|
|
44
|
+
instructions: params.systemPrompt,
|
|
45
|
+
input: inputItems,
|
|
46
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
47
|
+
stream: true,
|
|
48
|
+
}, { signal: params.signal });
|
|
49
|
+
let response;
|
|
50
|
+
const toolCalls = new Map();
|
|
51
|
+
for await (const event of stream) {
|
|
52
|
+
switch (event.type) {
|
|
53
|
+
case "response.output_text.delta":
|
|
54
|
+
yield { type: "assistant_text_delta", text: event.delta };
|
|
55
|
+
break;
|
|
56
|
+
case "response.reasoning_summary_text.delta":
|
|
57
|
+
yield { type: "assistant_reasoning_delta", text: event.delta };
|
|
58
|
+
break;
|
|
59
|
+
case "response.reasoning_summary.delta":
|
|
60
|
+
if (typeof event.delta === "string") {
|
|
61
|
+
yield { type: "assistant_reasoning_delta", text: event.delta };
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case "response.output_item.added":
|
|
65
|
+
case "response.output_item.done":
|
|
66
|
+
if (event.item.type === "function_call") {
|
|
67
|
+
const existing = toolCalls.get(event.output_index) ?? {};
|
|
68
|
+
existing.id = event.item.call_id;
|
|
69
|
+
existing.name = event.item.name;
|
|
70
|
+
existing.itemId = event.item.id;
|
|
71
|
+
toolCalls.set(event.output_index, existing);
|
|
72
|
+
yield {
|
|
73
|
+
type: "tool_call_delta",
|
|
74
|
+
index: event.output_index,
|
|
75
|
+
id: existing.id,
|
|
76
|
+
name: existing.name,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
case "response.function_call_arguments.delta": {
|
|
81
|
+
const existing = toolCalls.get(event.output_index);
|
|
82
|
+
yield {
|
|
83
|
+
type: "tool_call_delta",
|
|
84
|
+
index: event.output_index,
|
|
85
|
+
id: existing?.id,
|
|
86
|
+
name: existing?.name,
|
|
87
|
+
inputDelta: event.delta,
|
|
88
|
+
};
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "response.completed":
|
|
92
|
+
case "response.failed":
|
|
93
|
+
case "response.incomplete":
|
|
94
|
+
response = event.response;
|
|
95
|
+
break;
|
|
96
|
+
case "error":
|
|
97
|
+
throw new Error(event.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!response) {
|
|
101
|
+
throw new Error("OpenAI Responses stream completed without a final response.");
|
|
102
|
+
}
|
|
103
|
+
yield {
|
|
104
|
+
type: "response",
|
|
105
|
+
response: responseToModelResponse(response),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// ─── Input conversion ────────────────────────────────────────────────
|
|
110
|
+
/**
|
|
111
|
+
* Convert internal messages into Responses API input items.
|
|
112
|
+
*
|
|
113
|
+
* The Responses API accepts an array of input items. Our internal
|
|
114
|
+
* representation uses role-based messages with content blocks, so we map:
|
|
115
|
+
*
|
|
116
|
+
* - user text → { type: "message", role: "user", content: [...] }
|
|
117
|
+
* - assistant text → { type: "message", role: "assistant", content: [...] }
|
|
118
|
+
* - assistant tool_use → { type: "function_call", ... }
|
|
119
|
+
* - user tool_result → { type: "function_call_output", ... }
|
|
120
|
+
*/
|
|
121
|
+
export function messagesToInputItems(messages) {
|
|
122
|
+
const items = [];
|
|
123
|
+
for (const msg of messages) {
|
|
124
|
+
if (typeof msg.content === "string") {
|
|
125
|
+
items.push({
|
|
126
|
+
type: "message",
|
|
127
|
+
role: msg.role,
|
|
128
|
+
content: msg.content,
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const messageContentBlocks = msg.content.filter((b) => b.type === "text" || b.type === "image" || b.type === "file");
|
|
133
|
+
const toolUseBlocks = msg.content.filter((b) => b.type === "tool_use");
|
|
134
|
+
const toolResultBlocks = msg.content.filter((b) => b.type === "tool_result");
|
|
135
|
+
if (messageContentBlocks.length > 0) {
|
|
136
|
+
if (msg.role === "assistant") {
|
|
137
|
+
const textBlocks = messageContentBlocks.filter((b) => b.type === "text");
|
|
138
|
+
items.push({
|
|
139
|
+
type: "message",
|
|
140
|
+
role: "assistant",
|
|
141
|
+
content: textBlocks.map((b) => ({
|
|
142
|
+
type: "output_text",
|
|
143
|
+
text: b.text,
|
|
144
|
+
annotations: [],
|
|
145
|
+
})),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
items.push({
|
|
150
|
+
type: "message",
|
|
151
|
+
role: "user",
|
|
152
|
+
content: messageContentBlocks.map(toResponseInputContent),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const block of toolUseBlocks) {
|
|
157
|
+
const tu = block;
|
|
158
|
+
items.push({
|
|
159
|
+
type: "function_call",
|
|
160
|
+
call_id: tu.id,
|
|
161
|
+
name: tu.name,
|
|
162
|
+
arguments: JSON.stringify(tu.input),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const block of toolResultBlocks) {
|
|
166
|
+
const tr = block;
|
|
167
|
+
items.push({
|
|
168
|
+
type: "function_call_output",
|
|
169
|
+
call_id: tr.toolUseId,
|
|
170
|
+
output: tr.content,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return items;
|
|
175
|
+
}
|
|
176
|
+
export function conversationItemsToInputItems(conversationItems) {
|
|
177
|
+
const items = [];
|
|
178
|
+
for (const item of conversationItems) {
|
|
179
|
+
switch (item.type) {
|
|
180
|
+
case "message":
|
|
181
|
+
case "compaction_summary": {
|
|
182
|
+
const role = item.type === "message" ? item.role : "user";
|
|
183
|
+
const content = item.type === "message" ? item.content : item.summary;
|
|
184
|
+
items.push({
|
|
185
|
+
type: "message",
|
|
186
|
+
role,
|
|
187
|
+
content,
|
|
188
|
+
});
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case "attachment":
|
|
192
|
+
items.push({
|
|
193
|
+
type: "message",
|
|
194
|
+
role: "user",
|
|
195
|
+
content: [toResponseInputContent(item.attachment)],
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
case "reasoning":
|
|
199
|
+
if (!item.id)
|
|
200
|
+
break;
|
|
201
|
+
items.push({
|
|
202
|
+
type: "reasoning",
|
|
203
|
+
id: item.id,
|
|
204
|
+
summary: item.summary
|
|
205
|
+
? [{ type: "summary_text", text: item.summary }]
|
|
206
|
+
: [],
|
|
207
|
+
...(item.encryptedContent
|
|
208
|
+
? { encrypted_content: item.encryptedContent }
|
|
209
|
+
: {}),
|
|
210
|
+
});
|
|
211
|
+
break;
|
|
212
|
+
case "function_call":
|
|
213
|
+
items.push({
|
|
214
|
+
type: "function_call",
|
|
215
|
+
call_id: item.id,
|
|
216
|
+
name: item.name,
|
|
217
|
+
arguments: JSON.stringify(item.input),
|
|
218
|
+
});
|
|
219
|
+
break;
|
|
220
|
+
case "function_output":
|
|
221
|
+
items.push({
|
|
222
|
+
type: "function_call_output",
|
|
223
|
+
call_id: item.callId,
|
|
224
|
+
output: item.content,
|
|
225
|
+
});
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return items;
|
|
230
|
+
}
|
|
231
|
+
export function toFunctionTool(tool) {
|
|
232
|
+
return {
|
|
233
|
+
type: "function",
|
|
234
|
+
name: tool.name,
|
|
235
|
+
description: tool.description || undefined,
|
|
236
|
+
parameters: tool.parameters,
|
|
237
|
+
strict: false,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function toResponseInputContent(block) {
|
|
241
|
+
switch (block.type) {
|
|
242
|
+
case "text":
|
|
243
|
+
return { type: "input_text", text: block.text };
|
|
244
|
+
case "image":
|
|
245
|
+
return {
|
|
246
|
+
type: "input_image",
|
|
247
|
+
image_url: attachmentUrl(block),
|
|
248
|
+
detail: "auto",
|
|
249
|
+
};
|
|
250
|
+
case "file":
|
|
251
|
+
return {
|
|
252
|
+
type: "input_file",
|
|
253
|
+
filename: block.name,
|
|
254
|
+
...responseFileSource(block),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function attachmentUrl(block) {
|
|
259
|
+
if (block.source.type === "url")
|
|
260
|
+
return block.source.url;
|
|
261
|
+
return `data:${block.source.mediaType};base64,${block.source.data}`;
|
|
262
|
+
}
|
|
263
|
+
function responseFileSource(block) {
|
|
264
|
+
if (block.source.type === "url") {
|
|
265
|
+
return { file_url: block.source.url };
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
file_data: `data:${block.source.mediaType};base64,${block.source.data}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// ─── Output conversion ────────────────────────────────────────────────
|
|
272
|
+
/**
|
|
273
|
+
* Convert a Responses API response into our internal ModelResponse.
|
|
274
|
+
*
|
|
275
|
+
* Output items we handle:
|
|
276
|
+
* - message → text content blocks
|
|
277
|
+
* - function_call → tool_use content blocks
|
|
278
|
+
* - reasoning → reasoning string (summary text)
|
|
279
|
+
*
|
|
280
|
+
* We determine the stop reason from the response status:
|
|
281
|
+
* - completed + has function_calls → tool_use
|
|
282
|
+
* - completed → end_turn
|
|
283
|
+
* - incomplete + reason max_tokens → max_tokens
|
|
284
|
+
* - everything else → error
|
|
285
|
+
*/
|
|
286
|
+
export function responseToModelResponse(response) {
|
|
287
|
+
const content = [];
|
|
288
|
+
let reasoning;
|
|
289
|
+
const reasoningItems = [];
|
|
290
|
+
for (const item of response.output) {
|
|
291
|
+
if (item.type === "message") {
|
|
292
|
+
const message = item;
|
|
293
|
+
for (const part of message.content) {
|
|
294
|
+
if (part.type === "output_text") {
|
|
295
|
+
const textPart = part;
|
|
296
|
+
content.push({ type: "text", text: textPart.text });
|
|
297
|
+
}
|
|
298
|
+
// Skip refusals — we treat them as no content.
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if (item.type === "function_call") {
|
|
302
|
+
const fc = item;
|
|
303
|
+
content.push({
|
|
304
|
+
type: "tool_use",
|
|
305
|
+
id: fc.call_id,
|
|
306
|
+
name: fc.name,
|
|
307
|
+
input: parseFunctionCallArguments(fc.arguments),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
else if (item.type === "reasoning") {
|
|
311
|
+
const ri = item;
|
|
312
|
+
const summaryText = ri.summary
|
|
313
|
+
.map((s) => s.text)
|
|
314
|
+
.join("\n")
|
|
315
|
+
.trim();
|
|
316
|
+
if (summaryText) {
|
|
317
|
+
reasoning = summaryText;
|
|
318
|
+
}
|
|
319
|
+
reasoningItems.push({
|
|
320
|
+
type: "reasoning",
|
|
321
|
+
id: ri.id,
|
|
322
|
+
summary: summaryText,
|
|
323
|
+
...(ri.encrypted_content ? { encryptedContent: ri.encrypted_content } : {}),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
// Skip other output item types (web_search_call, etc.)
|
|
327
|
+
}
|
|
328
|
+
const stopReason = mapStopReason(response, content);
|
|
329
|
+
const usage = response.usage
|
|
330
|
+
? {
|
|
331
|
+
inputTokens: response.usage.input_tokens,
|
|
332
|
+
outputTokens: response.usage.output_tokens,
|
|
333
|
+
}
|
|
334
|
+
: undefined;
|
|
335
|
+
return {
|
|
336
|
+
stopReason,
|
|
337
|
+
content,
|
|
338
|
+
reasoning,
|
|
339
|
+
reasoningItems: reasoningItems.length > 0 ? reasoningItems : undefined,
|
|
340
|
+
usage,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
export function mapStopReason(response, content) {
|
|
344
|
+
// If we have tool_use blocks the model wants to call tools.
|
|
345
|
+
if (content.some((b) => b.type === "tool_use")) {
|
|
346
|
+
return "tool_use";
|
|
347
|
+
}
|
|
348
|
+
switch (response.status) {
|
|
349
|
+
case "completed":
|
|
350
|
+
return "end_turn";
|
|
351
|
+
case "incomplete": {
|
|
352
|
+
const reason = response.incomplete_details?.reason;
|
|
353
|
+
if (reason === "max_output_tokens") {
|
|
354
|
+
return "max_tokens";
|
|
355
|
+
}
|
|
356
|
+
return "error";
|
|
357
|
+
}
|
|
358
|
+
case "failed":
|
|
359
|
+
return "error";
|
|
360
|
+
default:
|
|
361
|
+
return "error";
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function parseFunctionCallArguments(inputJson) {
|
|
365
|
+
if (!inputJson)
|
|
366
|
+
return {};
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(inputJson);
|
|
369
|
+
return isPlainObject(parsed) ? parsed : {};
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return {};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function isPlainObject(value) {
|
|
376
|
+
return (typeof value === "object" && value !== null && !Array.isArray(value));
|
|
377
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ChatParams, ModelProvider, ModelStreamEvent } from "./provider.js";
|
|
2
|
+
import type { ModelResponse } from "../types/agent.js";
|
|
3
|
+
interface OpenAIProviderOptions {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
baseURL?: string;
|
|
6
|
+
maxTokens?: number;
|
|
7
|
+
openRouter?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class OpenAIProvider implements ModelProvider {
|
|
10
|
+
private client;
|
|
11
|
+
private model;
|
|
12
|
+
private maxTokens?;
|
|
13
|
+
private openRouter;
|
|
14
|
+
constructor(model: string, options?: OpenAIProviderOptions);
|
|
15
|
+
chat(params: ChatParams): Promise<ModelResponse>;
|
|
16
|
+
streamChat(params: ChatParams): AsyncIterable<ModelStreamEvent>;
|
|
17
|
+
private withOpenRouterSessionId;
|
|
18
|
+
private mapUsage;
|
|
19
|
+
private createChatCompletionStream;
|
|
20
|
+
private toOpenAIMessages;
|
|
21
|
+
private toOpenAITool;
|
|
22
|
+
private toOpenAIUserContent;
|
|
23
|
+
private attachmentUrl;
|
|
24
|
+
private fromOpenAIMessage;
|
|
25
|
+
private extractReasoning;
|
|
26
|
+
private extractReasoningDelta;
|
|
27
|
+
private mapFinishReason;
|
|
28
|
+
private parseToolArguments;
|
|
29
|
+
private isRecord;
|
|
30
|
+
private isUnsupportedStreamOptionsError;
|
|
31
|
+
}
|
|
32
|
+
export {};
|