toolnexus 0.9.5 → 0.12.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 +31 -1
- package/dist/agents/agent.d.ts +66 -0
- package/dist/agents/agent.js +101 -0
- package/dist/agents/compaction.d.ts +77 -0
- package/dist/agents/compaction.js +96 -0
- package/dist/agents/home.d.ts +90 -0
- package/dist/agents/home.js +203 -0
- package/dist/agents/index.d.ts +10 -0
- package/dist/agents/index.js +10 -0
- package/dist/agents/runtime.d.ts +372 -0
- package/dist/agents/runtime.js +737 -0
- package/dist/client.d.ts +31 -3
- package/dist/client.js +161 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/translate.d.ts +107 -0
- package/dist/translate.js +195 -0
- package/package.json +2 -2
- package/src/agents/agent.ts +137 -0
- package/src/agents/compaction.ts +148 -0
- package/src/agents/home.ts +250 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/runtime.ts +909 -0
- package/src/client.ts +182 -11
- package/src/index.ts +2 -0
- package/src/translate.ts +242 -0
package/src/client.ts
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
import type { Toolkit } from "./toolkit.js"
|
|
7
7
|
import type { ToolResult, Request, Answer } from "./types.js"
|
|
8
8
|
import { pendingOf } from "./types.js"
|
|
9
|
+
import type { TranslateRequest, TranslateResult, TranslatedToolCall } from "./translate.js"
|
|
10
|
+
import {
|
|
11
|
+
finishReasonFor,
|
|
12
|
+
contentText,
|
|
13
|
+
argsString,
|
|
14
|
+
openAIMessagesToAnthropic,
|
|
15
|
+
openAIToolsToAnthropic,
|
|
16
|
+
openAIToolChoiceToAnthropic,
|
|
17
|
+
hasSystemMessage,
|
|
18
|
+
} from "./translate.js"
|
|
9
19
|
|
|
10
20
|
export type ClientStyle = "openai" | "anthropic"
|
|
11
21
|
|
|
@@ -155,10 +165,14 @@ export interface RunResult {
|
|
|
155
165
|
usage: Usage
|
|
156
166
|
/** The model used. */
|
|
157
167
|
model: string
|
|
158
|
-
/** "done" normally; "pending" iff a tool suspended and no waitFor was configured (§10)
|
|
159
|
-
|
|
168
|
+
/** "done" normally; "pending" iff a tool suspended and no waitFor was configured (§10);
|
|
169
|
+
* "incomplete" iff a limit stopped the run loudly (§7D) — a maxTurns stop with no final
|
|
170
|
+
* text is never a silent "done". */
|
|
171
|
+
status: "done" | "pending" | "incomplete"
|
|
160
172
|
/** The unresolved suspension — present iff status === "pending" (§10). */
|
|
161
173
|
pending?: Request
|
|
174
|
+
/** The limit that stopped the run (e.g. "maxTurns") — present iff status === "incomplete". */
|
|
175
|
+
limit?: string
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
/** Events yielded by client.stream(). */
|
|
@@ -379,10 +393,11 @@ export class Client {
|
|
|
379
393
|
this.opts.onMetric?.(ev)
|
|
380
394
|
}
|
|
381
395
|
|
|
382
|
-
/** Emit the terminal `run` metric event and build the RunResult.
|
|
383
|
-
|
|
396
|
+
/** Emit the terminal `run` metric event and build the RunResult. `exhausted` marks a
|
|
397
|
+
* maxTurns loop exit — with no final text that is a LOUD "incomplete", never "done" (§7D). */
|
|
398
|
+
private endRun(runStart: number, text: string, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage, exhausted = false): RunResult {
|
|
384
399
|
this.emit({ event: "run", model: this.opts.model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart })
|
|
385
|
-
return this.result(text, messages, toolCalls, turns, usage)
|
|
400
|
+
return this.result(text, messages, toolCalls, turns, usage, exhausted)
|
|
386
401
|
}
|
|
387
402
|
|
|
388
403
|
/** Emit a `run` error metric event (once, on a thrown run). */
|
|
@@ -396,6 +411,159 @@ export class Client {
|
|
|
396
411
|
: this.runOpenAI(prompt, ctx.toolkit, ctx.signal, ctx.history)
|
|
397
412
|
}
|
|
398
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Single-turn translation (§11): exactly ONE provider call, returned in OpenAI shape.
|
|
416
|
+
* No agent loop, no tool execution, no conversation state — every call is
|
|
417
|
+
* self-contained, so this may be run statelessly and concurrently.
|
|
418
|
+
*
|
|
419
|
+
* The inbound half of the adapters: `toAnthropic`/`toGemini` send declarations out,
|
|
420
|
+
* this reads the provider's tool calls back in. A `toolkit` passed in the request is
|
|
421
|
+
* DECLARED and never executed.
|
|
422
|
+
*
|
|
423
|
+
* Retries/backoff, requestParams merging and the `llm` metric are shared with the loop.
|
|
424
|
+
* `beforeLLM`/`afterLLM` fire once; tool hooks do not, because no tool runs.
|
|
425
|
+
*/
|
|
426
|
+
async translate(req: TranslateRequest): Promise<TranslateResult> {
|
|
427
|
+
return this.opts.style === "anthropic" ? this.translateAnthropic(req) : this.translateOpenAI(req)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** OpenAI-style upstream: near-passthrough. */
|
|
431
|
+
private async translateOpenAI(req: TranslateRequest): Promise<TranslateResult> {
|
|
432
|
+
const key = resolveKey(this.opts)
|
|
433
|
+
const declared = [...(req.toolkit ? req.toolkit.toOpenAI() : []), ...(req.tools ?? [])]
|
|
434
|
+
let messages = (req.messages ?? []).slice()
|
|
435
|
+
const sys = req.system ?? this.opts.systemPrompt ?? ""
|
|
436
|
+
if (sys && !hasSystemMessage(messages)) messages = [{ role: "system", content: sys }, ...messages]
|
|
437
|
+
|
|
438
|
+
let tools = declared
|
|
439
|
+
if (this.opts.hooks?.beforeLLM) {
|
|
440
|
+
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn: 0 })
|
|
441
|
+
if (ov?.messages) messages = ov.messages
|
|
442
|
+
if (ov?.tools) tools = ov.tools
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const body: Record<string, any> = { model: this.opts.model, messages }
|
|
446
|
+
if (tools.length) body.tools = tools
|
|
447
|
+
if (req.toolChoice !== undefined) body.tool_choice = req.toolChoice
|
|
448
|
+
if (req.maxTokens) body.max_tokens = req.maxTokens
|
|
449
|
+
|
|
450
|
+
const signal = this.makeSignal(req.signal)
|
|
451
|
+
{
|
|
452
|
+
const data = await this.llmCallJson(
|
|
453
|
+
`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`,
|
|
454
|
+
{
|
|
455
|
+
method: "POST",
|
|
456
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${key}`, ...this.opts.headers },
|
|
457
|
+
body: JSON.stringify(this.finalizeBody(body)),
|
|
458
|
+
},
|
|
459
|
+
signal,
|
|
460
|
+
"openai",
|
|
461
|
+
)
|
|
462
|
+
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model: this.opts.model, turn: 0 })
|
|
463
|
+
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
464
|
+
addUsage(usage, data.usage, "openai")
|
|
465
|
+
const choice = data.choices?.[0]
|
|
466
|
+
const toolCalls: TranslatedToolCall[] = (choice?.message?.tool_calls ?? []).map((tc: any) => ({
|
|
467
|
+
id: String(tc.id ?? ""),
|
|
468
|
+
name: String(tc.function?.name ?? ""),
|
|
469
|
+
arguments: argsString(tc.function?.arguments),
|
|
470
|
+
}))
|
|
471
|
+
return {
|
|
472
|
+
text: choice?.message?.content ?? "",
|
|
473
|
+
toolCalls,
|
|
474
|
+
finishReason: choice?.finish_reason ?? finishReasonFor(toolCalls.length > 0),
|
|
475
|
+
usage,
|
|
476
|
+
model: this.opts.model,
|
|
477
|
+
raw: data,
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Anthropic-style upstream: the real translation. */
|
|
483
|
+
private async translateAnthropic(req: TranslateRequest): Promise<TranslateResult> {
|
|
484
|
+
const key = resolveKey(this.opts)
|
|
485
|
+
const base = this.opts.baseUrl.replace(/\/$/, "")
|
|
486
|
+
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`
|
|
487
|
+
|
|
488
|
+
const converted = openAIMessagesToAnthropic(req.messages ?? [])
|
|
489
|
+
let messages = converted.messages
|
|
490
|
+
const system = req.system ?? this.opts.systemPrompt ?? converted.system
|
|
491
|
+
let tools = [
|
|
492
|
+
...(req.toolkit ? req.toolkit.toAnthropic() : []),
|
|
493
|
+
...openAIToolsToAnthropic(req.tools),
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
if (this.opts.hooks?.beforeLLM) {
|
|
497
|
+
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn: 0 })
|
|
498
|
+
if (ov?.messages) messages = ov.messages
|
|
499
|
+
if (ov?.tools) tools = ov.tools
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const body: Record<string, any> = {
|
|
503
|
+
model: this.opts.model,
|
|
504
|
+
max_tokens: req.maxTokens && req.maxTokens > 0 ? req.maxTokens : 4096,
|
|
505
|
+
messages,
|
|
506
|
+
}
|
|
507
|
+
if (system) body.system = system
|
|
508
|
+
if (tools.length) body.tools = tools
|
|
509
|
+
const tc = openAIToolChoiceToAnthropic(req.toolChoice)
|
|
510
|
+
if (tc) body.tool_choice = tc
|
|
511
|
+
|
|
512
|
+
const signal = this.makeSignal(req.signal)
|
|
513
|
+
{
|
|
514
|
+
const data = await this.llmCallJson(
|
|
515
|
+
endpoint,
|
|
516
|
+
{
|
|
517
|
+
method: "POST",
|
|
518
|
+
headers: {
|
|
519
|
+
"content-type": "application/json",
|
|
520
|
+
"x-api-key": key,
|
|
521
|
+
"anthropic-version": "2023-06-01",
|
|
522
|
+
...this.opts.headers,
|
|
523
|
+
},
|
|
524
|
+
body: JSON.stringify(this.finalizeBody(body)),
|
|
525
|
+
},
|
|
526
|
+
signal,
|
|
527
|
+
"anthropic",
|
|
528
|
+
)
|
|
529
|
+
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model: this.opts.model, turn: 0 })
|
|
530
|
+
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
531
|
+
addUsage(usage, data.usage, "anthropic")
|
|
532
|
+
const text: string[] = []
|
|
533
|
+
const toolCalls: TranslatedToolCall[] = []
|
|
534
|
+
for (const b of data.content ?? []) {
|
|
535
|
+
if (b?.type === "text") text.push(b.text ?? "")
|
|
536
|
+
else if (b?.type === "tool_use") {
|
|
537
|
+
toolCalls.push({
|
|
538
|
+
id: String(b.id ?? ""),
|
|
539
|
+
name: String(b.name ?? ""),
|
|
540
|
+
arguments: JSON.stringify(b.input ?? {}),
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
text: text.join(""),
|
|
546
|
+
toolCalls,
|
|
547
|
+
finishReason: finishReasonFor(toolCalls.length > 0, data.stop_reason),
|
|
548
|
+
usage,
|
|
549
|
+
model: this.opts.model,
|
|
550
|
+
raw: data,
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Renders a translate result's tool calls as an OpenAI `tool_calls` array, ready to put
|
|
557
|
+
* on an assistant message. Convenience for assembling a response envelope.
|
|
558
|
+
*/
|
|
559
|
+
static toolCallsJSON(res: TranslateResult): any[] {
|
|
560
|
+
return res.toolCalls.map((tc) => ({
|
|
561
|
+
id: tc.id,
|
|
562
|
+
type: "function",
|
|
563
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
564
|
+
}))
|
|
565
|
+
}
|
|
566
|
+
|
|
399
567
|
/**
|
|
400
568
|
* Stateful ask. With an `id`, the client's `store` remembers the conversation:
|
|
401
569
|
* it loads that id's transcript, runs, saves the updated transcript, and returns
|
|
@@ -596,15 +764,18 @@ export class Client {
|
|
|
596
764
|
if (s.halted) return this.pendingRun(runStart, s.halted, messages, toolCalls, turns, usage)
|
|
597
765
|
}
|
|
598
766
|
}
|
|
599
|
-
return this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage)
|
|
767
|
+
return this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage, true)
|
|
600
768
|
} catch (e) {
|
|
601
769
|
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
602
770
|
throw e
|
|
603
771
|
}
|
|
604
772
|
}
|
|
605
773
|
|
|
606
|
-
private result(text: string, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage): RunResult {
|
|
607
|
-
|
|
774
|
+
private result(text: string, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage, exhausted = false): RunResult {
|
|
775
|
+
const incomplete = exhausted && text === ""
|
|
776
|
+
const r: RunResult = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model: this.opts.model, status: incomplete ? "incomplete" : "done" }
|
|
777
|
+
if (incomplete) r.limit = "maxTurns"
|
|
778
|
+
return r
|
|
608
779
|
}
|
|
609
780
|
|
|
610
781
|
/** §10: a run halted because a tool suspended and no `waitFor` was configured. */
|
|
@@ -702,7 +873,7 @@ export class Client {
|
|
|
702
873
|
messages.push({ role: "user", content })
|
|
703
874
|
if (halted) return this.pendingRun(runStart, halted, messages, toolCalls, turns, usage)
|
|
704
875
|
}
|
|
705
|
-
return this.endRun(runStart, "", messages, toolCalls, turns, usage)
|
|
876
|
+
return this.endRun(runStart, "", messages, toolCalls, turns, usage, true)
|
|
706
877
|
} catch (e) {
|
|
707
878
|
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
708
879
|
throw e
|
|
@@ -798,7 +969,7 @@ export class Client {
|
|
|
798
969
|
yield { type: "tool_result", id: c.id, name: c.name, output: result.output, isError: result.isError }
|
|
799
970
|
}
|
|
800
971
|
}
|
|
801
|
-
yield { type: "done", result: this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage) }
|
|
972
|
+
yield { type: "done", result: this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage, true) }
|
|
802
973
|
} catch (e) {
|
|
803
974
|
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
804
975
|
throw e
|
|
@@ -891,7 +1062,7 @@ export class Client {
|
|
|
891
1062
|
}
|
|
892
1063
|
messages.push({ role: "user", content: results })
|
|
893
1064
|
}
|
|
894
|
-
yield { type: "done", result: this.endRun(runStart, "", messages, toolCalls, turns, usage) }
|
|
1065
|
+
yield { type: "done", result: this.endRun(runStart, "", messages, toolCalls, turns, usage, true) }
|
|
895
1066
|
} catch (e) {
|
|
896
1067
|
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
897
1068
|
throw e
|
package/src/index.ts
CHANGED
package/src/translate.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-turn translation — the translator path (SPEC.md §11, ADR-0011).
|
|
3
|
+
*
|
|
4
|
+
* `client.translate(request)` is toolnexus used as a pure wire-format translator: OpenAI
|
|
5
|
+
* shapes in, exactly ONE provider call, OpenAI shapes out. No agent loop, no tool
|
|
6
|
+
* execution, no conversation state — so a caller can run it statelessly.
|
|
7
|
+
*
|
|
8
|
+
* It is the INBOUND half of the §5 adapters: `toOpenAI`/`toAnthropic`/`toGemini` send
|
|
9
|
+
* declarations out, this reads the provider's tool calls back in.
|
|
10
|
+
*
|
|
11
|
+
* Use it when the CALLER owns the conversation and executes tools itself (the standard
|
|
12
|
+
* OpenAI function-calling posture). When toolnexus owns the conversation, use the agent
|
|
13
|
+
* loop with relay tools + `runWithAnswer` (§10) instead.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Toolkit } from "./toolkit.js"
|
|
17
|
+
import type { Usage } from "./client.js"
|
|
18
|
+
|
|
19
|
+
/** An OpenAI-shaped chat request, handed over verbatim (§11). */
|
|
20
|
+
export interface TranslateRequest {
|
|
21
|
+
/**
|
|
22
|
+
* The OpenAI `messages` array, verbatim — including assistant turns carrying
|
|
23
|
+
* `tool_calls` and `tool`-role results carrying `tool_call_id`.
|
|
24
|
+
*/
|
|
25
|
+
messages: any[]
|
|
26
|
+
/**
|
|
27
|
+
* The OpenAI `tools` array, verbatim
|
|
28
|
+
* (`{type:"function",function:{name,description,parameters}}`). Declaration-only —
|
|
29
|
+
* nothing here is ever executed.
|
|
30
|
+
*/
|
|
31
|
+
tools?: any[]
|
|
32
|
+
/**
|
|
33
|
+
* Declares an ordinary toolkit's tools to the provider WITHOUT executing any of them —
|
|
34
|
+
* MCP tools, skills, native functions, A2A agents, builtins. Use this when you have a
|
|
35
|
+
* toolkit and want the model's tool CALLS handed back to you to dispatch yourself.
|
|
36
|
+
* Composes with `tools`; toolkit declarations come first.
|
|
37
|
+
*/
|
|
38
|
+
toolkit?: Toolkit
|
|
39
|
+
/** The OpenAI `tool_choice`, verbatim. Omitted when absent. */
|
|
40
|
+
toolChoice?: any
|
|
41
|
+
/** Overrides the system prompt. Absent uses any `system` message found in `messages`. */
|
|
42
|
+
system?: string
|
|
43
|
+
/** Overrides the per-provider default max tokens. */
|
|
44
|
+
maxTokens?: number
|
|
45
|
+
/** Aborts the in-flight provider call. */
|
|
46
|
+
signal?: AbortSignal
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One tool call the model asked for, in OpenAI shape (§11). */
|
|
50
|
+
export interface TranslatedToolCall {
|
|
51
|
+
/** The tool-call id the caller must echo on its `tool` result message. */
|
|
52
|
+
id: string
|
|
53
|
+
/** The function name. */
|
|
54
|
+
name: string
|
|
55
|
+
/** Arguments as a JSON **string** — the OpenAI wire form, echoable byte-for-byte. */
|
|
56
|
+
arguments: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** An OpenAI-shaped single-turn result (§11). */
|
|
60
|
+
export interface TranslateResult {
|
|
61
|
+
/** Assistant text ("" when the model only called tools). */
|
|
62
|
+
text: string
|
|
63
|
+
/** The tool calls the model emitted, in provider order. None is ever dropped. */
|
|
64
|
+
toolCalls: TranslatedToolCall[]
|
|
65
|
+
/** The OpenAI finish reason. A turn with any tool call is always `"tool_calls"`. */
|
|
66
|
+
finishReason: "stop" | "tool_calls" | "length" | "content_filter"
|
|
67
|
+
/** This single call's token usage. */
|
|
68
|
+
usage: Usage
|
|
69
|
+
/** The model that answered. */
|
|
70
|
+
model: string
|
|
71
|
+
/** The provider's decoded response, for fields this type does not model. */
|
|
72
|
+
raw?: any
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Maps a provider stop reason onto an OpenAI finish reason. Tool calls win: a turn that
|
|
77
|
+
* emitted any tool call is always `"tool_calls"` to a conforming client.
|
|
78
|
+
*/
|
|
79
|
+
export function finishReasonFor(hasToolCalls: boolean, providerStop?: string): TranslateResult["finishReason"] {
|
|
80
|
+
if (hasToolCalls) return "tool_calls"
|
|
81
|
+
switch (providerStop) {
|
|
82
|
+
case "max_tokens":
|
|
83
|
+
case "length":
|
|
84
|
+
return "length"
|
|
85
|
+
case "refusal":
|
|
86
|
+
case "content_filter":
|
|
87
|
+
return "content_filter"
|
|
88
|
+
default:
|
|
89
|
+
return "stop"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Flattens an OpenAI `content` value to text: the string form and the parts-array form. */
|
|
94
|
+
export function contentText(content: unknown): string {
|
|
95
|
+
if (typeof content === "string") return content
|
|
96
|
+
if (Array.isArray(content)) {
|
|
97
|
+
return content
|
|
98
|
+
.map((p) => (p && typeof p === "object" && typeof (p as any).text === "string" ? (p as any).text : ""))
|
|
99
|
+
.join("")
|
|
100
|
+
}
|
|
101
|
+
return ""
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Parses a tool-call arguments value into an object, tolerating both wire forms. */
|
|
105
|
+
export function argsObject(args: unknown): Record<string, unknown> {
|
|
106
|
+
if (args && typeof args === "object") return args as Record<string, unknown>
|
|
107
|
+
if (typeof args === "string" && args.trim()) {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(args)
|
|
110
|
+
if (parsed && typeof parsed === "object") return parsed
|
|
111
|
+
} catch {
|
|
112
|
+
/* fall through to {} — a malformed arguments string is not fatal */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Renders a tool-call arguments value as the JSON string the OpenAI wire format uses. */
|
|
119
|
+
export function argsString(args: unknown): string {
|
|
120
|
+
if (typeof args === "string") return args
|
|
121
|
+
if (args && typeof args === "object") return JSON.stringify(args)
|
|
122
|
+
return "{}"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Reads an assistant message's OpenAI `tool_calls`. */
|
|
126
|
+
export function toolCallsOf(m: any): TranslatedToolCall[] {
|
|
127
|
+
const raw = m?.tool_calls
|
|
128
|
+
if (!Array.isArray(raw)) return []
|
|
129
|
+
const out: TranslatedToolCall[] = []
|
|
130
|
+
for (const tc of raw) {
|
|
131
|
+
const fn = tc?.function
|
|
132
|
+
if (!fn) continue
|
|
133
|
+
out.push({ id: String(tc.id ?? ""), name: String(fn.name ?? ""), arguments: argsString(fn.arguments) })
|
|
134
|
+
}
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Converts an OpenAI `messages` array into Anthropic-native messages plus the extracted
|
|
140
|
+
* system prompt, preserving the tool structure a text flattening destroys (§11):
|
|
141
|
+
*
|
|
142
|
+
* - an assistant turn's `tool_calls` become `tool_use` blocks (arguments re-parsed to objects),
|
|
143
|
+
* - a `tool`-role result becomes a `tool_result` block keyed by `tool_call_id`, MERGED into a
|
|
144
|
+
* single user turn when consecutive (providers want one result-bearing turn per assistant turn),
|
|
145
|
+
* - `system`/`developer` messages are hoisted out, since Anthropic takes system separately.
|
|
146
|
+
*/
|
|
147
|
+
export function openAIMessagesToAnthropic(messages: any[]): { messages: any[]; system: string } {
|
|
148
|
+
const out: any[] = []
|
|
149
|
+
const systemParts: string[] = []
|
|
150
|
+
let pendingResults: any[] = []
|
|
151
|
+
|
|
152
|
+
const flush = () => {
|
|
153
|
+
if (pendingResults.length) {
|
|
154
|
+
out.push({ role: "user", content: pendingResults })
|
|
155
|
+
pendingResults = []
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const m of messages ?? []) {
|
|
160
|
+
if (!m || typeof m !== "object") continue
|
|
161
|
+
switch (m.role) {
|
|
162
|
+
case "system":
|
|
163
|
+
case "developer": {
|
|
164
|
+
flush()
|
|
165
|
+
const s = contentText(m.content)
|
|
166
|
+
if (s) systemParts.push(s)
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
case "tool":
|
|
170
|
+
case "function": {
|
|
171
|
+
const block: any = { type: "tool_result", content: contentText(m.content) }
|
|
172
|
+
if (m.tool_call_id) block.tool_use_id = String(m.tool_call_id)
|
|
173
|
+
pendingResults.push(block)
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
case "assistant": {
|
|
177
|
+
flush()
|
|
178
|
+
const blocks: any[] = []
|
|
179
|
+
const s = contentText(m.content)
|
|
180
|
+
if (s) blocks.push({ type: "text", text: s })
|
|
181
|
+
for (const tc of toolCallsOf(m)) {
|
|
182
|
+
blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: argsObject(tc.arguments) })
|
|
183
|
+
}
|
|
184
|
+
if (!blocks.length) break // an empty assistant turn would be rejected
|
|
185
|
+
out.push({ role: "assistant", content: blocks })
|
|
186
|
+
break
|
|
187
|
+
}
|
|
188
|
+
default: {
|
|
189
|
+
flush()
|
|
190
|
+
const s = contentText(m.content)
|
|
191
|
+
if (s) out.push({ role: "user", content: s })
|
|
192
|
+
else if (Array.isArray(m.content) && m.content.length) out.push({ role: "user", content: m.content })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
flush()
|
|
197
|
+
return { messages: out, system: systemParts.join("\n\n") }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Converts an OpenAI `tools` array into Anthropic tool declarations. Entries that are
|
|
202
|
+
* already provider-native pass through; anything unrecognized is skipped.
|
|
203
|
+
*/
|
|
204
|
+
export function openAIToolsToAnthropic(tools: any[] | undefined): any[] {
|
|
205
|
+
const out: any[] = []
|
|
206
|
+
for (const t of tools ?? []) {
|
|
207
|
+
if (!t || typeof t !== "object") continue
|
|
208
|
+
const fn = (t as any).function
|
|
209
|
+
if (!fn) {
|
|
210
|
+
if ((t as any).name) out.push(t) // already native
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
if (!fn.name) continue
|
|
214
|
+
const decl: any = { name: fn.name }
|
|
215
|
+
if (fn.description) decl.description = fn.description
|
|
216
|
+
decl.input_schema = fn.parameters ?? { type: "object", properties: {} }
|
|
217
|
+
out.push(decl)
|
|
218
|
+
}
|
|
219
|
+
return out
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Maps OpenAI `tool_choice` onto Anthropic's shape. Returns undefined for absent/"auto"
|
|
224
|
+
* (the provider default) and for anything unrecognized.
|
|
225
|
+
*/
|
|
226
|
+
export function openAIToolChoiceToAnthropic(choice: unknown): any {
|
|
227
|
+
if (typeof choice === "string") {
|
|
228
|
+
if (choice === "required" || choice === "any") return { type: "any" }
|
|
229
|
+
if (choice === "none") return { type: "none" }
|
|
230
|
+
return undefined
|
|
231
|
+
}
|
|
232
|
+
if (choice && typeof choice === "object") {
|
|
233
|
+
const name = (choice as any).function?.name
|
|
234
|
+
if (name) return { type: "tool", name }
|
|
235
|
+
}
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** True when the message list already carries a system-ish message. */
|
|
240
|
+
export function hasSystemMessage(messages: any[]): boolean {
|
|
241
|
+
return (messages ?? []).some((m) => m?.role === "system" || m?.role === "developer")
|
|
242
|
+
}
|