auggy 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/CHANGELOG.md +96 -0
- package/LICENSE +201 -0
- package/README.md +161 -0
- package/package.json +76 -0
- package/src/agent-card.ts +39 -0
- package/src/agent.ts +283 -0
- package/src/agentmail-client.ts +138 -0
- package/src/augments/bash/index.ts +463 -0
- package/src/augments/bash/skill/SKILL.md +156 -0
- package/src/augments/budgets/budget-store.ts +513 -0
- package/src/augments/budgets/index.ts +134 -0
- package/src/augments/budgets/preamble.ts +93 -0
- package/src/augments/budgets/types.ts +89 -0
- package/src/augments/file-memory/index.ts +71 -0
- package/src/augments/filesystem/index.ts +533 -0
- package/src/augments/filesystem/skill/SKILL.md +142 -0
- package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
- package/src/augments/layered-memory/extractor/buffer.ts +56 -0
- package/src/augments/layered-memory/extractor/frequency.ts +79 -0
- package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
- package/src/augments/layered-memory/extractor/parse.ts +75 -0
- package/src/augments/layered-memory/extractor/prompt.md +26 -0
- package/src/augments/layered-memory/index.ts +757 -0
- package/src/augments/layered-memory/skill/SKILL.md +153 -0
- package/src/augments/layered-memory/storage/migrations/README.md +16 -0
- package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
- package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
- package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
- package/src/augments/layered-memory/storage/types.ts +98 -0
- package/src/augments/link/index.ts +489 -0
- package/src/augments/link/translate.ts +261 -0
- package/src/augments/notify/adapters/agentmail.ts +70 -0
- package/src/augments/notify/adapters/telegram.ts +60 -0
- package/src/augments/notify/adapters/webhook.ts +55 -0
- package/src/augments/notify/index.ts +284 -0
- package/src/augments/notify/skill/SKILL.md +150 -0
- package/src/augments/org-context/index.ts +721 -0
- package/src/augments/org-context/skill/SKILL.md +96 -0
- package/src/augments/skills/index.ts +103 -0
- package/src/augments/supabase-memory/index.ts +151 -0
- package/src/augments/telegram-transport/index.ts +312 -0
- package/src/augments/telegram-transport/polling.ts +55 -0
- package/src/augments/telegram-transport/webhook.ts +56 -0
- package/src/augments/turn-control/index.ts +61 -0
- package/src/augments/turn-control/skill/SKILL.md +155 -0
- package/src/augments/visitor-auth/email-validation.ts +66 -0
- package/src/augments/visitor-auth/index.ts +779 -0
- package/src/augments/visitor-auth/rate-limiter.ts +90 -0
- package/src/augments/visitor-auth/skill/SKILL.md +55 -0
- package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
- package/src/augments/visitor-auth/storage/types.ts +164 -0
- package/src/augments/visitor-auth/types.ts +123 -0
- package/src/augments/visitor-auth/verify-page.ts +179 -0
- package/src/augments/web-fetch/index.ts +331 -0
- package/src/augments/web-fetch/skill/SKILL.md +100 -0
- package/src/cli/agent-index.ts +289 -0
- package/src/cli/augment-catalog.ts +320 -0
- package/src/cli/augment-resolver.ts +597 -0
- package/src/cli/commands/add-skill.ts +194 -0
- package/src/cli/commands/add.ts +87 -0
- package/src/cli/commands/chat.ts +207 -0
- package/src/cli/commands/create.ts +462 -0
- package/src/cli/commands/dev.ts +139 -0
- package/src/cli/commands/eval.ts +180 -0
- package/src/cli/commands/ls.ts +66 -0
- package/src/cli/commands/remove.ts +95 -0
- package/src/cli/commands/restart.ts +40 -0
- package/src/cli/commands/start.ts +123 -0
- package/src/cli/commands/status.ts +104 -0
- package/src/cli/commands/stop.ts +84 -0
- package/src/cli/commands/visitors-revoke.ts +155 -0
- package/src/cli/commands/visitors.ts +101 -0
- package/src/cli/config-parser.ts +1034 -0
- package/src/cli/engine-resolver.ts +68 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/model-picker.ts +89 -0
- package/src/cli/pid-registry.ts +146 -0
- package/src/cli/plist-generator.ts +117 -0
- package/src/cli/resolve-config.ts +56 -0
- package/src/cli/scaffold-skills.ts +158 -0
- package/src/cli/scaffold.ts +291 -0
- package/src/cli/skill-frontmatter.ts +51 -0
- package/src/cli/skill-validator.ts +151 -0
- package/src/cli/types.ts +228 -0
- package/src/cli/yaml-helpers.ts +66 -0
- package/src/engines/_shared/cost.ts +55 -0
- package/src/engines/_shared/schema-normalize.ts +75 -0
- package/src/engines/anthropic/pricing.ts +117 -0
- package/src/engines/anthropic.ts +483 -0
- package/src/engines/openai/pricing.ts +67 -0
- package/src/engines/openai.ts +446 -0
- package/src/engines/openrouter/pricing.ts +83 -0
- package/src/engines/openrouter.ts +185 -0
- package/src/helpers.ts +24 -0
- package/src/http.ts +387 -0
- package/src/index.ts +165 -0
- package/src/kernel/capability-table.ts +172 -0
- package/src/kernel/context-allocator.ts +161 -0
- package/src/kernel/history-manager.ts +198 -0
- package/src/kernel/lifecycle-manager.ts +106 -0
- package/src/kernel/output-validator.ts +35 -0
- package/src/kernel/preamble.ts +23 -0
- package/src/kernel/route-collector.ts +97 -0
- package/src/kernel/timeout.ts +21 -0
- package/src/kernel/tool-selector.ts +47 -0
- package/src/kernel/trace-emitter.ts +66 -0
- package/src/kernel/transport-queue.ts +147 -0
- package/src/kernel/turn-loop.ts +1148 -0
- package/src/memory/context-synthesis.ts +83 -0
- package/src/memory/memory-bus.ts +61 -0
- package/src/memory/registry.ts +80 -0
- package/src/memory/tools.ts +320 -0
- package/src/memory/types.ts +8 -0
- package/src/parts.ts +30 -0
- package/src/scaffold-templates/identity.md +31 -0
- package/src/telegram-client.ts +145 -0
- package/src/tokenizer.ts +14 -0
- package/src/transports/ag-ui-events.ts +253 -0
- package/src/transports/visitor-token.ts +82 -0
- package/src/transports/web-transport.ts +948 -0
- package/src/types.ts +1009 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { normalizeSchema } from "./_shared/schema-normalize";
|
|
3
|
+
import { lookup, getFreshness, priceOpenAIResponse } from "./openai/pricing";
|
|
4
|
+
import type {
|
|
5
|
+
AssembledPrompt,
|
|
6
|
+
Message,
|
|
7
|
+
ModelClient,
|
|
8
|
+
ModelDelta,
|
|
9
|
+
ModelResponse,
|
|
10
|
+
ToolDefinition,
|
|
11
|
+
} from "../types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* OpenAI engine — a ModelClient adapter that drives the agent's reasoning
|
|
15
|
+
* via OpenAI's Chat Completions API.
|
|
16
|
+
*
|
|
17
|
+
* Responsibilities:
|
|
18
|
+
* - Translate AssembledPrompt into Chat Completions request shape
|
|
19
|
+
* (system message, conversation messages, tools)
|
|
20
|
+
* - Run the API call (buffered; no streaming)
|
|
21
|
+
* - Translate the response back into ModelResponse
|
|
22
|
+
*
|
|
23
|
+
* The engine is stateless beyond the underlying SDK. Retries, timeouts, and
|
|
24
|
+
* rate-limit handling live in the SDK; everything above (queue, history,
|
|
25
|
+
* context budgeting) is the kernel's job.
|
|
26
|
+
*
|
|
27
|
+
* Token counting uses a character/4 approximation, matching the Anthropic
|
|
28
|
+
* engine and Auggy's default tokenizer. The kernel does not call this
|
|
29
|
+
* method on hot paths (it uses src/tokenizer.ts directly), so accuracy is
|
|
30
|
+
* not load-bearing.
|
|
31
|
+
*/
|
|
32
|
+
export interface OpenAIEngineOptions {
|
|
33
|
+
/** API key. Defaults to OPENAI_API_KEY env (read by the SDK). */
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
/** Model ID (e.g. "gpt-5", "gpt-5.1", "o3", "o4-mini"). */
|
|
36
|
+
model: string;
|
|
37
|
+
/** Total context window in tokens. Defaults to 128_000.
|
|
38
|
+
* Set this per model — the kernel uses it for context budgeting and
|
|
39
|
+
* doesn't validate against the actual API limit. */
|
|
40
|
+
maxContextTokens?: number;
|
|
41
|
+
/** Per-turn output cap, sent as `max_completion_tokens`. Defaults to 4096.
|
|
42
|
+
* Note: `max_tokens` is deprecated in v6 SDK and rejected by o-series. */
|
|
43
|
+
maxTokens?: number;
|
|
44
|
+
/** Optional base URL override (for proxies or compatible providers). */
|
|
45
|
+
baseURL?: string;
|
|
46
|
+
/** Reasoning effort for reasoning-capable models.
|
|
47
|
+
* - `none`: gpt-5.1 only
|
|
48
|
+
* - `minimal | low | medium | high`: universally supported on reasoning models
|
|
49
|
+
* - `xhigh`: gpt-5.1-codex-max and later
|
|
50
|
+
* Older Chat Completions models (e.g. gpt-4) do NOT support this — the API
|
|
51
|
+
* returns an error which propagates through `complete()`. */
|
|
52
|
+
reasoningEffort?: OpenAI.Chat.ChatCompletionReasoningEffort;
|
|
53
|
+
/**
|
|
54
|
+
* Override pricing for cost estimation. If set, the adapter uses these rates
|
|
55
|
+
* instead of the built-in pricing table. Useful for unknown models or custom
|
|
56
|
+
* pricing arrangements. USD per million tokens.
|
|
57
|
+
*
|
|
58
|
+
* Accepts the full Pricing shape; cache fields are accepted for type symmetry
|
|
59
|
+
* with Anthropic but not used by the OpenAI adapter today (no cache-token
|
|
60
|
+
* usage is parsed from OpenAI Chat Completions responses).
|
|
61
|
+
*/
|
|
62
|
+
costOverride?: import("./_shared/cost").Pricing;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createOpenAIEngine(opts: OpenAIEngineOptions): ModelClient {
|
|
66
|
+
const client = new OpenAI({
|
|
67
|
+
apiKey: opts.apiKey,
|
|
68
|
+
baseURL: opts.baseURL,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const maxContextTokens = opts.maxContextTokens ?? 128_000;
|
|
72
|
+
const maxOutputTokens = opts.maxTokens ?? 4096;
|
|
73
|
+
|
|
74
|
+
// Pricing freshness + availability warning at startup. Fires once at
|
|
75
|
+
// factory time, not per-turn.
|
|
76
|
+
if (!opts.costOverride) {
|
|
77
|
+
const rates = lookup(opts.model);
|
|
78
|
+
if (!rates) {
|
|
79
|
+
// eslint-disable-next-line no-console
|
|
80
|
+
console.warn(
|
|
81
|
+
`[engines/openai] No pricing entry for model "${opts.model}" and no costOverride configured. ` +
|
|
82
|
+
`costUsd will be undefined; dailyBudgetUsd cannot enforce against this model. ` +
|
|
83
|
+
`Add the model to src/engines/openai/pricing.ts or configure engine.costOverride in agent.yaml.`,
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
const f = getFreshness();
|
|
87
|
+
if (f.stale) {
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.warn(
|
|
90
|
+
`[engines/openai] Pricing table verifiedAt ${f.verifiedAt} is more than 90 days old. ` +
|
|
91
|
+
`Cost estimates may be drifting from actual billing. Verify rates and update src/engines/openai/pricing.ts.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else if (
|
|
96
|
+
opts.costOverride.cacheWriteUsdPerMtok !== undefined ||
|
|
97
|
+
opts.costOverride.cacheReadUsdPerMtok !== undefined
|
|
98
|
+
) {
|
|
99
|
+
// Operator set cache rates on OpenAI override. Today's adapter does not
|
|
100
|
+
// parse cache tokens from OpenAI Chat Completions responses, so cache
|
|
101
|
+
// rates would be silently ignored. Warn loudly rather than silently
|
|
102
|
+
// under-report — operators provisioning these rates should know they
|
|
103
|
+
// don't take effect.
|
|
104
|
+
// eslint-disable-next-line no-console
|
|
105
|
+
console.warn(
|
|
106
|
+
`[engines/openai] costOverride.cacheWriteUsdPerMtok/cacheReadUsdPerMtok set but ignored — ` +
|
|
107
|
+
`the OpenAI adapter does not parse cache tokens from upstream responses. Cache rates will not contribute to costUsd.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
maxContextTokens,
|
|
113
|
+
|
|
114
|
+
countTokens(text: string): number {
|
|
115
|
+
return Math.ceil(text.length / 4);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async complete(
|
|
119
|
+
prompt: AssembledPrompt,
|
|
120
|
+
_opts?: { onDelta?: (delta: ModelDelta) => void },
|
|
121
|
+
): Promise<ModelResponse> {
|
|
122
|
+
const systemMessage = assembleOpenAISystemMessage(prompt);
|
|
123
|
+
const messages = convertOpenAIMessages(prompt.messages);
|
|
124
|
+
const tools = convertOpenAITools(prompt.tools);
|
|
125
|
+
|
|
126
|
+
const allMessages: OpenAI.Chat.ChatCompletionMessageParam[] = systemMessage
|
|
127
|
+
? [systemMessage, ...messages]
|
|
128
|
+
: messages;
|
|
129
|
+
|
|
130
|
+
const params: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming = {
|
|
131
|
+
model: opts.model,
|
|
132
|
+
max_completion_tokens: maxOutputTokens,
|
|
133
|
+
messages: allMessages,
|
|
134
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
135
|
+
...(opts.reasoningEffort ? { reasoning_effort: opts.reasoningEffort } : {}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
let completion: OpenAI.Chat.ChatCompletion;
|
|
139
|
+
try {
|
|
140
|
+
completion = await client.chat.completions.create(params);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
// Wrap the SDK error so logs identify which engine + model failed,
|
|
143
|
+
// not just "OpenAIError: 429". `cause` preserves the original SDK
|
|
144
|
+
// error (including `.status`) for callers that introspect.
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
+
throw new Error(`OpenAI engine (${opts.model}) failed: ${msg}`, {
|
|
147
|
+
cause: err,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const response = buildOpenAIModelResponse(completion, opts.model);
|
|
151
|
+
const result = priceOpenAIResponse(opts.model, opts.costOverride, {
|
|
152
|
+
prompt_tokens: completion.usage?.prompt_tokens ?? response.inputTokens,
|
|
153
|
+
completion_tokens: completion.usage?.completion_tokens ?? response.outputTokens,
|
|
154
|
+
reasoning_tokens: (completion.usage as Record<string, unknown> | null | undefined)
|
|
155
|
+
?.reasoning_tokens as number | undefined,
|
|
156
|
+
});
|
|
157
|
+
return result.priced
|
|
158
|
+
? { ...response, costUsd: result.costUsd }
|
|
159
|
+
: { ...response, costUsd: undefined, unpricedReason: result.reason };
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ===========================================================================
|
|
165
|
+
// AssembledPrompt → OpenAI request translation
|
|
166
|
+
// ===========================================================================
|
|
167
|
+
|
|
168
|
+
/** Join system + context + assistant-preamble blocks into a single system
|
|
169
|
+
* message, or return null if there is nothing to say. OpenAI has no
|
|
170
|
+
* separate slot for context-block content, so it folds into system. */
|
|
171
|
+
export function assembleOpenAISystemMessage(
|
|
172
|
+
prompt: AssembledPrompt,
|
|
173
|
+
): OpenAI.Chat.ChatCompletionSystemMessageParam | null {
|
|
174
|
+
const parts: string[] = [];
|
|
175
|
+
if (prompt.systemBlocks.length > 0) {
|
|
176
|
+
parts.push(prompt.systemBlocks.join("\n\n"));
|
|
177
|
+
}
|
|
178
|
+
if (prompt.contextBlocks.length > 0) {
|
|
179
|
+
parts.push(prompt.contextBlocks.join("\n\n"));
|
|
180
|
+
}
|
|
181
|
+
if (prompt.assistantPreamble && prompt.assistantPreamble.length > 0) {
|
|
182
|
+
parts.push(prompt.assistantPreamble.join("\n\n"));
|
|
183
|
+
}
|
|
184
|
+
if (parts.length === 0) return null;
|
|
185
|
+
return { role: "system", content: parts.join("\n\n") };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Parse the JSON-stringified tool-call payload that the kernel writes to
|
|
189
|
+
* `Message.content` for tool_use messages. The kernel writes
|
|
190
|
+
* `JSON.stringify({ name, arguments: object })` (see turn-loop.ts:518),
|
|
191
|
+
* so the recovered shape is `{ name: string, arguments: Record }`.
|
|
192
|
+
* Returns null defensively on malformed JSON. */
|
|
193
|
+
export function safeParseToolCall(
|
|
194
|
+
content: string,
|
|
195
|
+
): { name: string; arguments: Record<string, unknown> } | null {
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(content) as {
|
|
198
|
+
name?: unknown;
|
|
199
|
+
arguments?: unknown;
|
|
200
|
+
};
|
|
201
|
+
if (
|
|
202
|
+
parsed &&
|
|
203
|
+
typeof parsed.name === "string" &&
|
|
204
|
+
parsed.arguments &&
|
|
205
|
+
typeof parsed.arguments === "object" &&
|
|
206
|
+
!Array.isArray(parsed.arguments)
|
|
207
|
+
) {
|
|
208
|
+
return {
|
|
209
|
+
name: parsed.name,
|
|
210
|
+
arguments: parsed.arguments as Record<string, unknown>,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
/* fall through */
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Walk Auggy's flat message list and produce OpenAI's grouped format.
|
|
220
|
+
*
|
|
221
|
+
* Auggy stores tool_use and tool_result as separate flat messages with
|
|
222
|
+
* matching toolCallIds. OpenAI wants:
|
|
223
|
+
* - `tool_use` entries folded into the preceding assistant message's
|
|
224
|
+
* `tool_calls` array
|
|
225
|
+
* - `tool_result` entries each emitted as a standalone `role: "tool"`
|
|
226
|
+
* message (no batching needed — OpenAI accepts consecutive tool messages)
|
|
227
|
+
*
|
|
228
|
+
* Consecutive `user` messages are coalesced (joined with \n\n) since OpenAI
|
|
229
|
+
* expects user/assistant alternation in most contexts.
|
|
230
|
+
*/
|
|
231
|
+
export function convertOpenAIMessages(
|
|
232
|
+
messages: Message[],
|
|
233
|
+
): OpenAI.Chat.ChatCompletionMessageParam[] {
|
|
234
|
+
const result: OpenAI.Chat.ChatCompletionMessageParam[] = [];
|
|
235
|
+
let i = 0;
|
|
236
|
+
|
|
237
|
+
while (i < messages.length) {
|
|
238
|
+
const msg = messages[i]!;
|
|
239
|
+
|
|
240
|
+
if (msg.role === "user") {
|
|
241
|
+
result.push({ role: "user", content: msg.content });
|
|
242
|
+
i++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (msg.role === "assistant") {
|
|
247
|
+
const text = msg.content;
|
|
248
|
+
const toolCalls: OpenAI.Chat.ChatCompletionMessageFunctionToolCall[] = [];
|
|
249
|
+
i++;
|
|
250
|
+
while (i < messages.length && messages[i]!.role === "tool_use") {
|
|
251
|
+
const tu = messages[i]!;
|
|
252
|
+
const parsed = safeParseToolCall(tu.content);
|
|
253
|
+
if (parsed && tu.toolCallId) {
|
|
254
|
+
toolCalls.push({
|
|
255
|
+
id: tu.toolCallId,
|
|
256
|
+
type: "function",
|
|
257
|
+
function: {
|
|
258
|
+
name: parsed.name,
|
|
259
|
+
arguments: JSON.stringify(parsed.arguments),
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
// Malformed history is a kernel-side bug or storage corruption.
|
|
264
|
+
// Drop the entry rather than fail the turn, but emit a warning so
|
|
265
|
+
// operators can grep for `[Auggy:openai]` and trace the cause.
|
|
266
|
+
warnDroppedToolUse(tu, parsed);
|
|
267
|
+
}
|
|
268
|
+
i++;
|
|
269
|
+
}
|
|
270
|
+
const assistantMsg: OpenAI.Chat.ChatCompletionAssistantMessageParam = {
|
|
271
|
+
role: "assistant",
|
|
272
|
+
content: text || null,
|
|
273
|
+
};
|
|
274
|
+
if (toolCalls.length > 0) {
|
|
275
|
+
assistantMsg.tool_calls = toolCalls;
|
|
276
|
+
}
|
|
277
|
+
// OpenAI rejects assistant messages that have neither content nor tool_calls.
|
|
278
|
+
if (assistantMsg.content || assistantMsg.tool_calls) {
|
|
279
|
+
result.push(assistantMsg);
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (msg.role === "tool_use") {
|
|
285
|
+
// Orphaned tool_use (no preceding assistant text). Emit as a standalone
|
|
286
|
+
// assistant message with only the tool call.
|
|
287
|
+
const parsed = safeParseToolCall(msg.content);
|
|
288
|
+
if (parsed && msg.toolCallId) {
|
|
289
|
+
result.push({
|
|
290
|
+
role: "assistant",
|
|
291
|
+
content: null,
|
|
292
|
+
tool_calls: [
|
|
293
|
+
{
|
|
294
|
+
id: msg.toolCallId,
|
|
295
|
+
type: "function",
|
|
296
|
+
function: {
|
|
297
|
+
name: parsed.name,
|
|
298
|
+
arguments: JSON.stringify(parsed.arguments),
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
warnDroppedToolUse(msg, parsed);
|
|
305
|
+
}
|
|
306
|
+
i++;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (msg.role === "tool_result") {
|
|
311
|
+
if (msg.toolCallId) {
|
|
312
|
+
result.push({
|
|
313
|
+
role: "tool",
|
|
314
|
+
tool_call_id: msg.toolCallId,
|
|
315
|
+
content: msg.content,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
i++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Unknown role — skip defensively.
|
|
323
|
+
i++;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return coalesceConsecutiveUsers(result);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Surface dropped tool_use entries to operators. The kernel writes these
|
|
330
|
+
* payloads itself, so a malformed entry indicates a kernel bug or storage
|
|
331
|
+
* corruption — silent drops would let the agent re-call tools without
|
|
332
|
+
* knowing. We log instead of throwing because failing the entire turn
|
|
333
|
+
* over one bad history entry is too aggressive. */
|
|
334
|
+
function warnDroppedToolUse(
|
|
335
|
+
m: Message,
|
|
336
|
+
parsed: { name: string; arguments: Record<string, unknown> } | null,
|
|
337
|
+
): void {
|
|
338
|
+
const reason = parsed === null ? "parse failed" : "missing toolCallId";
|
|
339
|
+
const preview = m.content.slice(0, 80);
|
|
340
|
+
// eslint-disable-next-line no-console
|
|
341
|
+
console.warn(
|
|
342
|
+
`[Auggy:openai] dropping malformed tool_use msg=${m.id} reason=${reason} content=${JSON.stringify(preview)}`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function coalesceConsecutiveUsers(
|
|
347
|
+
messages: OpenAI.Chat.ChatCompletionMessageParam[],
|
|
348
|
+
): OpenAI.Chat.ChatCompletionMessageParam[] {
|
|
349
|
+
if (messages.length <= 1) return messages;
|
|
350
|
+
const out: OpenAI.Chat.ChatCompletionMessageParam[] = [messages[0]!];
|
|
351
|
+
for (let i = 1; i < messages.length; i++) {
|
|
352
|
+
const prev = out[out.length - 1]!;
|
|
353
|
+
const curr = messages[i]!;
|
|
354
|
+
if (prev.role === "user" && curr.role === "user") {
|
|
355
|
+
const prevContent = typeof prev.content === "string" ? prev.content : "";
|
|
356
|
+
const currContent = typeof curr.content === "string" ? curr.content : "";
|
|
357
|
+
(prev as OpenAI.Chat.ChatCompletionUserMessageParam).content =
|
|
358
|
+
`${prevContent}\n\n${currContent}`;
|
|
359
|
+
} else {
|
|
360
|
+
out.push(curr);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Convert Auggy ToolDefinitions to OpenAI Chat Completions tool format.
|
|
367
|
+
* Schema normalization strips JSON Schema metadata keys (`$schema`, `$id`)
|
|
368
|
+
* that the API ignores or rejects. */
|
|
369
|
+
export function convertOpenAITools(toolDefs: ToolDefinition[]): OpenAI.Chat.ChatCompletionTool[] {
|
|
370
|
+
return toolDefs.map((td) => ({
|
|
371
|
+
type: "function",
|
|
372
|
+
function: {
|
|
373
|
+
name: td.name,
|
|
374
|
+
description: td.description,
|
|
375
|
+
parameters: normalizeSchema(td.inputSchema),
|
|
376
|
+
},
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ===========================================================================
|
|
381
|
+
// OpenAI response → ModelResponse translation
|
|
382
|
+
// ===========================================================================
|
|
383
|
+
|
|
384
|
+
/** Defensive JSON.parse that returns `{}` on malformed input. Used to recover
|
|
385
|
+
* tool call argument objects from the SDK's string-form `function.arguments`. */
|
|
386
|
+
export function safeParseJson(s: string): Record<string, unknown> {
|
|
387
|
+
try {
|
|
388
|
+
const parsed = JSON.parse(s);
|
|
389
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
390
|
+
return parsed as Record<string, unknown>;
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
/* fall through */
|
|
394
|
+
}
|
|
395
|
+
return {};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function buildOpenAIModelResponse(
|
|
399
|
+
completion: OpenAI.Chat.ChatCompletion,
|
|
400
|
+
modelLabel: string = "openai",
|
|
401
|
+
): ModelResponse {
|
|
402
|
+
const choice = completion.choices[0];
|
|
403
|
+
if (!choice) {
|
|
404
|
+
// Empty choices array — rare API edge case (typically a content policy
|
|
405
|
+
// rejection that was caught before generation). We throw rather than
|
|
406
|
+
// return an empty response because a silent empty turn means the agent
|
|
407
|
+
// sends nothing back to the user with no explanation. The thrown error
|
|
408
|
+
// wraps up through the kernel's transport layer.
|
|
409
|
+
throw new Error(
|
|
410
|
+
`OpenAI engine (${modelLabel}) returned no choices in completion ` +
|
|
411
|
+
`(usage=${JSON.stringify(completion.usage)}, id=${completion.id ?? "?"}). ` +
|
|
412
|
+
`Likely a content-policy rejection — inspect the prompt for blocked content.`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const message = choice.message;
|
|
417
|
+
const content = message.content ?? "";
|
|
418
|
+
const toolCalls = (message.tool_calls ?? [])
|
|
419
|
+
.map((tc) => {
|
|
420
|
+
// OpenAI v6 may return either function or custom tool calls. Auggy
|
|
421
|
+
// only emits function tools, so non-function results are dropped.
|
|
422
|
+
if (tc.type === "function") {
|
|
423
|
+
return {
|
|
424
|
+
name: tc.function.name,
|
|
425
|
+
arguments: safeParseJson(tc.function.arguments),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
})
|
|
430
|
+
.filter((x): x is { name: string; arguments: Record<string, unknown> } => x !== null);
|
|
431
|
+
|
|
432
|
+
const finishReason: ModelResponse["finishReason"] =
|
|
433
|
+
choice.finish_reason === "tool_calls" || choice.finish_reason === "function_call"
|
|
434
|
+
? "tool_use"
|
|
435
|
+
: choice.finish_reason === "length"
|
|
436
|
+
? "max_tokens"
|
|
437
|
+
: "end_turn";
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
content,
|
|
441
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
442
|
+
inputTokens: completion.usage?.prompt_tokens ?? 0,
|
|
443
|
+
outputTokens: completion.usage?.completion_tokens ?? 0,
|
|
444
|
+
finishReason,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Pricing,
|
|
3
|
+
type CostResult,
|
|
4
|
+
type PricingFreshness,
|
|
5
|
+
computeCostUsd,
|
|
6
|
+
} from "../_shared/cost";
|
|
7
|
+
import * as anthropicPricing from "../anthropic/pricing";
|
|
8
|
+
import * as openaiPricing from "../openai/pricing";
|
|
9
|
+
|
|
10
|
+
interface ResolvedPricing {
|
|
11
|
+
rates: Pricing;
|
|
12
|
+
resolvedProvider: "anthropic" | "openai" | "openrouter";
|
|
13
|
+
freshness: PricingFreshness;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve an OpenRouter model slug ("<provider>/<model>") to a pricing entry.
|
|
18
|
+
*
|
|
19
|
+
* v0 SCOPE: anthropic/* and openai/* slugs only. Other providers return null.
|
|
20
|
+
* The resolved freshness binds to the upstream provider's table verifiedAt,
|
|
21
|
+
* not OpenRouter's own (which has no table entries).
|
|
22
|
+
*/
|
|
23
|
+
export function resolveSlug(model: string): ResolvedPricing | null {
|
|
24
|
+
const slashIdx = model.indexOf("/");
|
|
25
|
+
if (slashIdx === -1) return null;
|
|
26
|
+
const prefix = model.slice(0, slashIdx);
|
|
27
|
+
const tail = model.slice(slashIdx + 1);
|
|
28
|
+
|
|
29
|
+
if (prefix === "anthropic") {
|
|
30
|
+
const rates = anthropicPricing.lookup(tail);
|
|
31
|
+
if (!rates) return null;
|
|
32
|
+
return { rates, resolvedProvider: "anthropic", freshness: anthropicPricing.getFreshness() };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (prefix === "openai") {
|
|
36
|
+
const rates = openaiPricing.lookup(tail);
|
|
37
|
+
if (!rates) return null;
|
|
38
|
+
return { rates, resolvedProvider: "openai", freshness: openaiPricing.getFreshness() };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface OpenRouterUsage {
|
|
45
|
+
prompt_tokens: number;
|
|
46
|
+
completion_tokens: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Price an OpenRouter response.
|
|
51
|
+
*
|
|
52
|
+
* Resolution order:
|
|
53
|
+
* 1. If costOverride is set, use it directly.
|
|
54
|
+
* 2. Try to resolve the model slug via anthropic/* or openai/* delegation.
|
|
55
|
+
* 3. If neither applies, return unpriced (out of v0 scope).
|
|
56
|
+
*/
|
|
57
|
+
export function priceOpenRouterResponse(
|
|
58
|
+
model: string,
|
|
59
|
+
override: Pricing | undefined,
|
|
60
|
+
usage: OpenRouterUsage,
|
|
61
|
+
): CostResult {
|
|
62
|
+
if (override) {
|
|
63
|
+
const costUsd = computeCostUsd(override, {
|
|
64
|
+
inputTokens: usage.prompt_tokens,
|
|
65
|
+
outputTokens: usage.completion_tokens,
|
|
66
|
+
});
|
|
67
|
+
return { priced: true, costUsd };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const resolved = resolveSlug(model);
|
|
71
|
+
if (!resolved) {
|
|
72
|
+
return {
|
|
73
|
+
priced: false,
|
|
74
|
+
reason: `openrouter: slug "${model}" outside v0 scope (anthropic/* and openai/* only)`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const costUsd = computeCostUsd(resolved.rates, {
|
|
79
|
+
inputTokens: usage.prompt_tokens,
|
|
80
|
+
outputTokens: usage.completion_tokens,
|
|
81
|
+
});
|
|
82
|
+
return { priced: true, costUsd };
|
|
83
|
+
}
|