@acentric/llm 0.0.0-stage → 0.1.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 +128 -2
- package/dist/catalog/anthropic.d.ts +77 -0
- package/dist/catalog/anthropic.d.ts.map +1 -0
- package/dist/catalog/anthropic.js +73 -0
- package/dist/catalog/anthropic.js.map +1 -0
- package/dist/catalog/fireworks.d.ts +93 -0
- package/dist/catalog/fireworks.d.ts.map +1 -0
- package/dist/catalog/fireworks.js +117 -0
- package/dist/catalog/fireworks.js.map +1 -0
- package/dist/catalog/openai.d.ts +103 -0
- package/dist/catalog/openai.d.ts.map +1 -0
- package/dist/catalog/openai.js +102 -0
- package/dist/catalog/openai.js.map +1 -0
- package/dist/client/anthropic/errors.d.ts +4 -0
- package/dist/client/anthropic/errors.d.ts.map +1 -0
- package/dist/client/anthropic/errors.js +80 -0
- package/dist/client/anthropic/errors.js.map +1 -0
- package/dist/client/anthropic/request.d.ts +9 -0
- package/dist/client/anthropic/request.d.ts.map +1 -0
- package/dist/client/anthropic/request.js +232 -0
- package/dist/client/anthropic/request.js.map +1 -0
- package/dist/client/anthropic/response.d.ts +13 -0
- package/dist/client/anthropic/response.d.ts.map +1 -0
- package/dist/client/anthropic/response.js +207 -0
- package/dist/client/anthropic/response.js.map +1 -0
- package/dist/client/direct.d.ts +18 -0
- package/dist/client/direct.d.ts.map +1 -0
- package/dist/client/direct.js +104 -0
- package/dist/client/direct.js.map +1 -0
- package/dist/client/gateway.d.ts +43 -0
- package/dist/client/gateway.d.ts.map +1 -0
- package/dist/client/gateway.js +72 -0
- package/dist/client/gateway.js.map +1 -0
- package/dist/client/http.d.ts +41 -0
- package/dist/client/http.d.ts.map +1 -0
- package/dist/client/http.js +190 -0
- package/dist/client/http.js.map +1 -0
- package/dist/client/llm.d.ts +41 -0
- package/dist/client/llm.d.ts.map +1 -0
- package/dist/client/llm.js +100 -0
- package/dist/client/llm.js.map +1 -0
- package/dist/client/responses/errors.d.ts +13 -0
- package/dist/client/responses/errors.d.ts.map +1 -0
- package/dist/client/responses/errors.js +144 -0
- package/dist/client/responses/errors.js.map +1 -0
- package/dist/client/responses/profile.d.ts +50 -0
- package/dist/client/responses/profile.d.ts.map +1 -0
- package/dist/client/responses/profile.js +4 -0
- package/dist/client/responses/profile.js.map +1 -0
- package/dist/client/responses/providers.d.ts +4 -0
- package/dist/client/responses/providers.d.ts.map +1 -0
- package/dist/client/responses/providers.js +75 -0
- package/dist/client/responses/providers.js.map +1 -0
- package/dist/client/responses/request.d.ts +5 -0
- package/dist/client/responses/request.d.ts.map +1 -0
- package/dist/client/responses/request.js +248 -0
- package/dist/client/responses/request.js.map +1 -0
- package/dist/client/responses/response.d.ts +10 -0
- package/dist/client/responses/response.d.ts.map +1 -0
- package/dist/client/responses/response.js +176 -0
- package/dist/client/responses/response.js.map +1 -0
- package/dist/client/util.d.ts +21 -0
- package/dist/client/util.d.ts.map +1 -0
- package/dist/client/util.js +63 -0
- package/dist/client/util.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -0
- package/dist/content.d.ts +38 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +5 -0
- package/dist/content.js.map +1 -0
- package/dist/convert/anthropic.d.ts +3 -0
- package/dist/convert/anthropic.d.ts.map +1 -0
- package/dist/convert/anthropic.js +114 -0
- package/dist/convert/anthropic.js.map +1 -0
- package/dist/convert/index.d.ts +12 -0
- package/dist/convert/index.d.ts.map +1 -0
- package/dist/convert/index.js +221 -0
- package/dist/convert/index.js.map +1 -0
- package/dist/convert/responses.d.ts +3 -0
- package/dist/convert/responses.d.ts.map +1 -0
- package/dist/convert/responses.js +197 -0
- package/dist/convert/responses.js.map +1 -0
- package/dist/convert/types.d.ts +88 -0
- package/dist/convert/types.d.ts.map +1 -0
- package/dist/convert/types.js +3 -0
- package/dist/convert/types.js.map +1 -0
- package/dist/cost.d.ts +18 -0
- package/dist/cost.d.ts.map +1 -0
- package/dist/cost.js +49 -0
- package/dist/cost.js.map +1 -0
- package/dist/failure.d.ts +90 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +111 -0
- package/dist/failure.js.map +1 -0
- package/dist/fields.d.ts +14 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +3 -0
- package/dist/fields.js.map +1 -0
- package/dist/formats/anthropic.d.ts +152 -0
- package/dist/formats/anthropic.d.ts.map +1 -0
- package/dist/formats/anthropic.js +7 -0
- package/dist/formats/anthropic.js.map +1 -0
- package/dist/formats/responses.d.ts +179 -0
- package/dist/formats/responses.d.ts.map +1 -0
- package/dist/formats/responses.js +9 -0
- package/dist/formats/responses.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +6 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +10 -0
- package/dist/json.js.map +1 -0
- package/dist/messages.d.ts +82 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +4 -0
- package/dist/messages.js.map +1 -0
- package/dist/model.d.ts +36 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +4 -0
- package/dist/model.js.map +1 -0
- package/dist/models.d.ts +9 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +19 -0
- package/dist/models.js.map +1 -0
- package/dist/providers/anthropic.d.ts +130 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +5 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/fireworks.d.ts +108 -0
- package/dist/providers/fireworks.d.ts.map +1 -0
- package/dist/providers/fireworks.js +6 -0
- package/dist/providers/fireworks.js.map +1 -0
- package/dist/providers/openai.d.ts +193 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +4 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers.d.ts +50 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +5 -0
- package/dist/providers.js.map +1 -0
- package/dist/read/anthropic.d.ts +15 -0
- package/dist/read/anthropic.d.ts.map +1 -0
- package/dist/read/anthropic.js +56 -0
- package/dist/read/anthropic.js.map +1 -0
- package/dist/read/responses.d.ts +26 -0
- package/dist/read/responses.d.ts.map +1 -0
- package/dist/read/responses.js +121 -0
- package/dist/read/responses.js.map +1 -0
- package/dist/read.d.ts +32 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +73 -0
- package/dist/read.js.map +1 -0
- package/dist/request.d.ts +23 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +4 -0
- package/dist/request.js.map +1 -0
- package/dist/response.d.ts +38 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +4 -0
- package/dist/response.js.map +1 -0
- package/dist/secrets.d.ts +22 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +35 -0
- package/dist/secrets.js.map +1 -0
- package/dist/tool-calls.d.ts +28 -0
- package/dist/tool-calls.d.ts.map +1 -0
- package/dist/tool-calls.js +4 -0
- package/dist/tool-calls.js.map +1 -0
- package/dist/tools.d.ts +33 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +4 -0
- package/dist/tools.js.map +1 -0
- package/dist/usage.d.ts +31 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +3 -0
- package/dist/usage.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +33 -3
- package/src/catalog/anthropic.ts +83 -0
- package/src/catalog/fireworks.ts +121 -0
- package/src/catalog/openai.ts +133 -0
- package/src/client/anthropic/errors.ts +88 -0
- package/src/client/anthropic/request.ts +272 -0
- package/src/client/anthropic/response.ts +236 -0
- package/src/client/direct.ts +157 -0
- package/src/client/gateway.ts +120 -0
- package/src/client/http.ts +231 -0
- package/src/client/llm.ts +151 -0
- package/src/client/responses/errors.ts +166 -0
- package/src/client/responses/profile.ts +55 -0
- package/src/client/responses/providers.ts +87 -0
- package/src/client/responses/request.ts +326 -0
- package/src/client/responses/response.ts +203 -0
- package/src/client/util.ts +81 -0
- package/src/client.ts +20 -0
- package/src/content.ts +46 -0
- package/src/convert/anthropic.ts +115 -0
- package/src/convert/index.ts +275 -0
- package/src/convert/responses.ts +203 -0
- package/src/convert/types.ts +104 -0
- package/src/cost.ts +71 -0
- package/src/failure.ts +164 -0
- package/src/fields.ts +13 -0
- package/src/formats/anthropic.ts +191 -0
- package/src/formats/responses.ts +207 -0
- package/src/index.ts +44 -0
- package/src/json.ts +13 -0
- package/src/messages.ts +99 -0
- package/src/model.ts +41 -0
- package/src/models.ts +23 -0
- package/src/providers/anthropic.ts +137 -0
- package/src/providers/fireworks.ts +140 -0
- package/src/providers/openai.ts +275 -0
- package/src/providers.ts +59 -0
- package/src/read/anthropic.ts +64 -0
- package/src/read/responses.ts +159 -0
- package/src/read.ts +108 -0
- package/src/request.ts +28 -0
- package/src/response.ts +44 -0
- package/src/secrets.ts +40 -0
- package/src/tool-calls.ts +33 -0
- package/src/tools.ts +38 -0
- package/src/usage.ts +33 -0
- package/src/version.ts +6 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// The Responses format's side of conversion (OpenAI, Fireworks).
|
|
2
|
+
|
|
3
|
+
import type { Json, JsonObject } from '@acentric/types';
|
|
4
|
+
|
|
5
|
+
import { isArray, isObject } from '../json.js';
|
|
6
|
+
import type { Common, Context, FormatConverter } from './types.js';
|
|
7
|
+
|
|
8
|
+
function texts(value: Json | undefined): string[] {
|
|
9
|
+
return isArray(value)
|
|
10
|
+
? value.flatMap((part) =>
|
|
11
|
+
isObject(part) && typeof part['text'] === 'string' ? [part['text']] : [],
|
|
12
|
+
)
|
|
13
|
+
: [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A reasoning item's readable text: its summary, or else its reasoning text. */
|
|
17
|
+
function reasoningText(item: JsonObject): string {
|
|
18
|
+
const summary = texts(item['summary']);
|
|
19
|
+
return (summary.length > 0 ? summary : texts(item['content'])).join('\n\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function message(content: JsonObject[]): JsonObject {
|
|
23
|
+
return { type: 'message', role: 'assistant', content };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function outputText(text: string): JsonObject {
|
|
27
|
+
return { type: 'output_text', text, annotations: [] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Reasoning the target cannot use as it is: dropped, or kept as text. */
|
|
31
|
+
function carryReasoning(text: string, context: Context): JsonObject | undefined {
|
|
32
|
+
if (context.reasoning === 'drop' || text === '') {
|
|
33
|
+
context.change('dropped', 'reasoning', text === '' ? 'no readable text' : 'bound to its model');
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (context.target.plainReasoning) {
|
|
37
|
+
context.change('rewritten', 'reasoning', 'as a plain reasoning item');
|
|
38
|
+
return { type: 'reasoning', summary: [{ type: 'summary_text', text }] };
|
|
39
|
+
}
|
|
40
|
+
context.change('rewritten', 'reasoning', 'as assistant text');
|
|
41
|
+
return message([outputText(text)]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** An item without the IDs and status its provider gave it. */
|
|
45
|
+
function withoutIds(item: JsonObject): JsonObject {
|
|
46
|
+
const { id: _id, status: _status, ...rest } = item;
|
|
47
|
+
return rest;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function string(value: Json | undefined, otherwise: string): string {
|
|
51
|
+
return typeof value === 'string' ? value : otherwise;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const responsesConverter: FormatConverter = {
|
|
55
|
+
sameFormat(items, sameProvider, context) {
|
|
56
|
+
const out: JsonObject[] = [];
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
switch (item['type']) {
|
|
59
|
+
case 'message':
|
|
60
|
+
out.push(sameProvider ? item : withoutIds(item));
|
|
61
|
+
break;
|
|
62
|
+
case 'reasoning': {
|
|
63
|
+
// A provider's own plain reasoning reads the same on any of its models.
|
|
64
|
+
const encrypted = item['encrypted_content'];
|
|
65
|
+
if (sameProvider && context.target.plainReasoning && !encrypted) {
|
|
66
|
+
out.push(item);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const carried = carryReasoning(reasoningText(item), context);
|
|
70
|
+
if (carried !== undefined) out.push(carried);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'function_call':
|
|
74
|
+
out.push(
|
|
75
|
+
sameProvider
|
|
76
|
+
? item
|
|
77
|
+
: {
|
|
78
|
+
type: 'function_call',
|
|
79
|
+
call_id: string(item['call_id'], ''),
|
|
80
|
+
name: string(item['name'], ''),
|
|
81
|
+
arguments: string(item['arguments'], '{}'),
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
break;
|
|
85
|
+
case 'custom_tool_call':
|
|
86
|
+
if (context.target.customTools) {
|
|
87
|
+
out.push(sameProvider ? item : withoutIds(item));
|
|
88
|
+
} else {
|
|
89
|
+
const callId = item['call_id'];
|
|
90
|
+
if (typeof callId === 'string') context.droppedCalls.add(callId);
|
|
91
|
+
context.change('dropped', 'tool_call', `custom tool ${string(item['name'], '')}`);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
if (sameProvider) out.push(item);
|
|
96
|
+
else context.change('dropped', 'provider_item', string(item['type'], 'unknown'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
read(items, context) {
|
|
103
|
+
const common: Common[] = [];
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
switch (item['type']) {
|
|
106
|
+
case 'message': {
|
|
107
|
+
const content = item['content'];
|
|
108
|
+
for (const part of isArray(content) ? content : []) {
|
|
109
|
+
if (!isObject(part)) continue;
|
|
110
|
+
const text = part['text'];
|
|
111
|
+
const refusal = part['refusal'];
|
|
112
|
+
if (part['type'] === 'output_text' && typeof text === 'string') {
|
|
113
|
+
const annotations = part['annotations'];
|
|
114
|
+
if (isArray(annotations) && annotations.length > 0) {
|
|
115
|
+
context.change('dropped', 'citations', `${annotations.length} annotations`);
|
|
116
|
+
}
|
|
117
|
+
common.push({ kind: 'text', text });
|
|
118
|
+
} else if (part['type'] === 'refusal' && typeof refusal === 'string') {
|
|
119
|
+
common.push({ kind: 'refusal', text: refusal });
|
|
120
|
+
} else {
|
|
121
|
+
common.push({ kind: 'other', type: `message ${string(part['type'], 'part')}` });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'reasoning':
|
|
127
|
+
common.push({ kind: 'reasoning', text: reasoningText(item) });
|
|
128
|
+
break;
|
|
129
|
+
case 'function_call':
|
|
130
|
+
common.push({
|
|
131
|
+
kind: 'call',
|
|
132
|
+
id: string(item['call_id'], ''),
|
|
133
|
+
name: string(item['name'], ''),
|
|
134
|
+
arguments: string(item['arguments'], '{}'),
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
case 'custom_tool_call':
|
|
138
|
+
common.push({
|
|
139
|
+
kind: 'custom_call',
|
|
140
|
+
id: string(item['call_id'], ''),
|
|
141
|
+
name: string(item['name'], ''),
|
|
142
|
+
input: string(item['input'], ''),
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
common.push({ kind: 'other', type: string(item['type'], 'unknown') });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return common;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
write(items, context) {
|
|
153
|
+
const out: JsonObject[] = [];
|
|
154
|
+
let open: JsonObject[] = [];
|
|
155
|
+
const close = () => {
|
|
156
|
+
if (open.length > 0) out.push(message(open));
|
|
157
|
+
open = [];
|
|
158
|
+
};
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
switch (item.kind) {
|
|
161
|
+
case 'text':
|
|
162
|
+
open.push(outputText(item.text));
|
|
163
|
+
break;
|
|
164
|
+
case 'refusal':
|
|
165
|
+
open.push({ type: 'refusal', refusal: item.text });
|
|
166
|
+
break;
|
|
167
|
+
case 'reasoning': {
|
|
168
|
+
close();
|
|
169
|
+
const carried = carryReasoning(item.text, context);
|
|
170
|
+
if (carried !== undefined) out.push(carried);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'call':
|
|
174
|
+
close();
|
|
175
|
+
out.push({
|
|
176
|
+
type: 'function_call',
|
|
177
|
+
call_id: context.callId(item.id),
|
|
178
|
+
name: item.name,
|
|
179
|
+
arguments: item.arguments,
|
|
180
|
+
});
|
|
181
|
+
break;
|
|
182
|
+
case 'custom_call':
|
|
183
|
+
close();
|
|
184
|
+
if (context.target.customTools) {
|
|
185
|
+
out.push({
|
|
186
|
+
type: 'custom_tool_call',
|
|
187
|
+
call_id: context.callId(item.id),
|
|
188
|
+
name: item.name,
|
|
189
|
+
input: item.input,
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
context.droppedCalls.add(item.id);
|
|
193
|
+
context.change('dropped', 'tool_call', `custom tool ${item.name}`);
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
case 'other':
|
|
197
|
+
context.change('dropped', 'provider_item', item.type);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
close();
|
|
201
|
+
return out;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The shapes conversion works with.
|
|
2
|
+
|
|
3
|
+
import type { JsonObject } from '@acentric/types';
|
|
4
|
+
|
|
5
|
+
import type { ApiFormat, Provider } from '../providers.js';
|
|
6
|
+
|
|
7
|
+
/** The provider and model a conversation is converted for. */
|
|
8
|
+
export type ConvertTarget<P extends Provider = Provider> = {
|
|
9
|
+
readonly provider: P;
|
|
10
|
+
readonly model: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ConvertOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* Reasoning the target cannot use as it is: another model's signed or
|
|
16
|
+
* encrypted reasoning, and any reasoning sent to another provider. `drop`,
|
|
17
|
+
* the default, leaves it out. `text` keeps its readable text: as a plain
|
|
18
|
+
* reasoning item where the target reads one (Fireworks), otherwise as
|
|
19
|
+
* assistant text. Reasoning with no readable text is dropped either way.
|
|
20
|
+
*/
|
|
21
|
+
readonly reasoning?: 'drop' | 'text';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** One thing `convert` dropped or rewrote. */
|
|
25
|
+
export type ConversionChange = {
|
|
26
|
+
/** The message's index in the input. */
|
|
27
|
+
readonly index: number;
|
|
28
|
+
readonly action: 'dropped' | 'rewritten';
|
|
29
|
+
readonly what:
|
|
30
|
+
| 'message'
|
|
31
|
+
| 'reasoning'
|
|
32
|
+
| 'tool_call'
|
|
33
|
+
| 'tool_result'
|
|
34
|
+
| 'tool_call_id'
|
|
35
|
+
| 'tool_arguments'
|
|
36
|
+
| 'provider_item'
|
|
37
|
+
| 'citations'
|
|
38
|
+
| 'refusal'
|
|
39
|
+
| 'part'
|
|
40
|
+
| 'part_option';
|
|
41
|
+
/** What exactly, for logs, such as `web_search_call` or `cache_control`. */
|
|
42
|
+
readonly detail: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The converted messages, and every change made to get them. */
|
|
46
|
+
export type Conversion<M> = {
|
|
47
|
+
readonly messages: M[];
|
|
48
|
+
readonly changes: readonly ConversionChange[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** What a target provider takes, as far as conversion cares. */
|
|
52
|
+
export type Capabilities = {
|
|
53
|
+
readonly api: ApiFormat;
|
|
54
|
+
/** Free-form custom tools and their calls. */
|
|
55
|
+
readonly customTools: boolean;
|
|
56
|
+
/** File parts. */
|
|
57
|
+
readonly files: boolean;
|
|
58
|
+
/** Images and files by a provider's file ID. */
|
|
59
|
+
readonly fileIds: boolean;
|
|
60
|
+
/** Reads reasoning items that hold only text. */
|
|
61
|
+
readonly plainReasoning: boolean;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type Target = Capabilities & ConvertTarget;
|
|
65
|
+
|
|
66
|
+
/** One assistant item read without its format, on its way to another format. Never stored. */
|
|
67
|
+
export type Common =
|
|
68
|
+
| { readonly kind: 'text'; readonly text: string }
|
|
69
|
+
| { readonly kind: 'refusal'; readonly text: string }
|
|
70
|
+
| { readonly kind: 'reasoning'; readonly text: string }
|
|
71
|
+
| {
|
|
72
|
+
readonly kind: 'call';
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly arguments: string;
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
readonly kind: 'custom_call';
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly input: string;
|
|
82
|
+
}
|
|
83
|
+
| { readonly kind: 'other'; readonly type: string };
|
|
84
|
+
|
|
85
|
+
/** What a conversation's conversion shares from message to message. */
|
|
86
|
+
export type Context = {
|
|
87
|
+
readonly target: Target;
|
|
88
|
+
readonly reasoning: 'drop' | 'text';
|
|
89
|
+
/** Call IDs whose calls were dropped; their results are dropped too. */
|
|
90
|
+
readonly droppedCalls: Set<string>;
|
|
91
|
+
change(action: ConversionChange['action'], what: ConversionChange['what'], detail: string): void;
|
|
92
|
+
/** A call ID as the target takes it. Results are mapped the same way. */
|
|
93
|
+
callId(id: string): string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** One API format's side of conversion. */
|
|
97
|
+
export type FormatConverter = {
|
|
98
|
+
/** The items of a message from another provider or model in this same format. */
|
|
99
|
+
sameFormat(items: readonly JsonObject[], sameProvider: boolean, context: Context): JsonObject[];
|
|
100
|
+
/** The items of a message in this format, read for another format. */
|
|
101
|
+
read(items: readonly JsonObject[], context: Context): Common[];
|
|
102
|
+
/** Common items written as this format's. */
|
|
103
|
+
write(items: readonly Common[], context: Context): JsonObject[];
|
|
104
|
+
};
|
package/src/cost.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// What a completion cost, estimated from its tokens and the model's prices,
|
|
2
|
+
// for providers that do not say what they charged.
|
|
3
|
+
|
|
4
|
+
import type { ModelCost, ModelPricing } from './model.js';
|
|
5
|
+
import type { Usage, UsageCost } from './usage.js';
|
|
6
|
+
|
|
7
|
+
export type CostOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Scales every price, for where and how the request ran, such as 0.5 for a
|
|
10
|
+
* batch or 1.1 for inference kept in the US. Defaults to 1.
|
|
11
|
+
*/
|
|
12
|
+
readonly multiplier?: number;
|
|
13
|
+
/** The part of `usage.cacheWrite` written for an hour, priced at `cacheWrite1h`. */
|
|
14
|
+
readonly cacheWrite1h?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The estimated cost, or undefined when it cannot be known: no prices, a count
|
|
19
|
+
* that is missing, or tokens in a part the prices do not cover. The prices for
|
|
20
|
+
* the whole request are those of the highest tier its prompt is over.
|
|
21
|
+
*/
|
|
22
|
+
export function estimateCost(
|
|
23
|
+
usage: Usage,
|
|
24
|
+
pricing: ModelPricing | undefined,
|
|
25
|
+
{ multiplier = 1, cacheWrite1h = 0 }: CostOptions = {},
|
|
26
|
+
): UsageCost | undefined {
|
|
27
|
+
const { totalInput, input, output, cacheRead, cacheWrite } = usage;
|
|
28
|
+
if (pricing === undefined || totalInput === undefined || input === undefined) return undefined;
|
|
29
|
+
if (output === undefined) return undefined;
|
|
30
|
+
if (cacheWrite1h > (cacheWrite ?? 0)) return undefined;
|
|
31
|
+
|
|
32
|
+
let prices: ModelCost = pricing.base;
|
|
33
|
+
for (const tier of pricing.tiers ?? []) {
|
|
34
|
+
if (totalInput > tier.aboveInputTokens) prices = tier.cost;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const read = part(cacheRead, prices.cacheRead);
|
|
38
|
+
const shortWrite = part(
|
|
39
|
+
cacheWrite === undefined ? undefined : cacheWrite - cacheWrite1h,
|
|
40
|
+
prices.cacheWrite,
|
|
41
|
+
);
|
|
42
|
+
const hourWrite = part(cacheWrite1h, prices.cacheWrite1h);
|
|
43
|
+
if (read === null || shortWrite === null || hourWrite === null) return undefined;
|
|
44
|
+
const written =
|
|
45
|
+
shortWrite === undefined && hourWrite === undefined
|
|
46
|
+
? undefined
|
|
47
|
+
: (shortWrite ?? 0) + (hourWrite ?? 0);
|
|
48
|
+
|
|
49
|
+
const parts = {
|
|
50
|
+
input: price(input, prices.input) * multiplier,
|
|
51
|
+
output: price(output, prices.output) * multiplier,
|
|
52
|
+
...(read === undefined ? {} : { cacheRead: read * multiplier }),
|
|
53
|
+
...(written === undefined ? {} : { cacheWrite: written * multiplier }),
|
|
54
|
+
};
|
|
55
|
+
const total = parts.input + parts.output + (parts.cacheRead ?? 0) + (parts.cacheWrite ?? 0);
|
|
56
|
+
return { source: 'estimated', ...parts, total };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The price of some tokens: undefined when there are none to price, null when they have no price. */
|
|
60
|
+
function part(
|
|
61
|
+
tokens: number | undefined,
|
|
62
|
+
perMillion: number | undefined,
|
|
63
|
+
): number | undefined | null {
|
|
64
|
+
if (tokens === undefined) return undefined;
|
|
65
|
+
if (tokens === 0) return perMillion === undefined ? undefined : 0;
|
|
66
|
+
return perMillion === undefined ? null : price(tokens, perMillion);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function price(tokens: number, perMillion: number): number {
|
|
70
|
+
return (tokens * perMillion) / 1_000_000;
|
|
71
|
+
}
|
package/src/failure.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Every way a completion can fail, as an error the client throws and as the
|
|
2
|
+
// Json a step reads. An LlmError's toJSON() is what a runner reports as the
|
|
3
|
+
// failure's error (RUNNERS.md §8), so step learns its kind, whether a retry
|
|
4
|
+
// may help and how long to wait. Retrying is the agent's decision.
|
|
5
|
+
|
|
6
|
+
import type { Json, Outcome } from '@acentric/types';
|
|
7
|
+
|
|
8
|
+
import { isObject } from './json.js';
|
|
9
|
+
import type { Usage } from './usage.js';
|
|
10
|
+
|
|
11
|
+
/** Whether each kind is worth retrying when the error does not say otherwise. */
|
|
12
|
+
const RETRYABLE = {
|
|
13
|
+
/** The client, the gateway or the provider refused the request. */
|
|
14
|
+
invalid_request: false,
|
|
15
|
+
/** Credentials missing, not valid or not allowed. */
|
|
16
|
+
auth: false,
|
|
17
|
+
/** Throttled; see `retryAfterMs`. */
|
|
18
|
+
rate_limited: true,
|
|
19
|
+
/** Out of credits or over a billing limit. */
|
|
20
|
+
quota_exceeded: false,
|
|
21
|
+
/** The prompt is too long for the model. */
|
|
22
|
+
context_overflow: false,
|
|
23
|
+
/** A provider's safety policy refused it; sending it again as it is will not help. */
|
|
24
|
+
policy_violation: false,
|
|
25
|
+
/** An unknown model, or a previous response that expired or is not valid; see `providerParam`. */
|
|
26
|
+
not_found: false,
|
|
27
|
+
/** The provider is out of capacity. */
|
|
28
|
+
overloaded: true,
|
|
29
|
+
/** Any other failure at the provider. */
|
|
30
|
+
provider_error: true,
|
|
31
|
+
/** The provider answered with something that cannot be read. */
|
|
32
|
+
invalid_response: false,
|
|
33
|
+
/** The provider or the gateway could not be reached. */
|
|
34
|
+
network_error: true,
|
|
35
|
+
/** The call took longer than allowed. */
|
|
36
|
+
timeout: true,
|
|
37
|
+
/** The handler's signal fired: the effect was cancelled, aborted or timed out. */
|
|
38
|
+
cancelled: false,
|
|
39
|
+
/** A bug in the gateway. */
|
|
40
|
+
internal_error: false,
|
|
41
|
+
} as const satisfies { readonly [kind: string]: boolean };
|
|
42
|
+
|
|
43
|
+
export type LlmFailureKind = keyof typeof RETRYABLE;
|
|
44
|
+
|
|
45
|
+
export const LLM_FAILURE_KINDS: readonly LlmFailureKind[] = Object.keys(RETRYABLE).filter(
|
|
46
|
+
(kind): kind is LlmFailureKind => kind in RETRYABLE,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/** A failed completion as Json: what an LlmError's toJSON() returns, and what step reads. */
|
|
50
|
+
export type LlmFailure = {
|
|
51
|
+
/** Always `LlmError`, which tells an LLM failure from other thrown values. */
|
|
52
|
+
readonly name: 'LlmError';
|
|
53
|
+
readonly message: string;
|
|
54
|
+
readonly kind: LlmFailureKind;
|
|
55
|
+
/** Whether sending the same request again may succeed. */
|
|
56
|
+
readonly retryable: boolean;
|
|
57
|
+
/** Left out for a failure before a provider was chosen. */
|
|
58
|
+
readonly provider?: string;
|
|
59
|
+
readonly providerCode?: string;
|
|
60
|
+
readonly providerType?: string;
|
|
61
|
+
/** The request field the provider blamed, such as `previous_response_id`. */
|
|
62
|
+
readonly providerParam?: string;
|
|
63
|
+
/** The provider's error as it answered, cut short when long. */
|
|
64
|
+
readonly providerError?: Json;
|
|
65
|
+
/** The provider's HTTP status, not the gateway's. */
|
|
66
|
+
readonly httpStatus?: number;
|
|
67
|
+
/** How long the provider asked to wait before trying again, in milliseconds. */
|
|
68
|
+
readonly retryAfterMs?: number;
|
|
69
|
+
/** The provider's request ID, for its support. */
|
|
70
|
+
readonly requestId?: string;
|
|
71
|
+
/** The provider's response ID, when a response was created before it failed. */
|
|
72
|
+
readonly responseId?: string;
|
|
73
|
+
/** Tokens the provider reported, and may bill, for a failed generation. */
|
|
74
|
+
readonly usage?: Usage;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type Detail = Omit<LlmFailure, 'name' | 'message' | 'kind' | 'retryable'>;
|
|
78
|
+
|
|
79
|
+
/** What an LlmError is made from. `retryable` defaults by kind. */
|
|
80
|
+
export type LlmErrorOptions = Detail & {
|
|
81
|
+
readonly kind: LlmFailureKind;
|
|
82
|
+
readonly retryable?: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const DETAIL: readonly (keyof Detail)[] = [
|
|
86
|
+
'provider',
|
|
87
|
+
'providerCode',
|
|
88
|
+
'providerType',
|
|
89
|
+
'providerParam',
|
|
90
|
+
'providerError',
|
|
91
|
+
'httpStatus',
|
|
92
|
+
'retryAfterMs',
|
|
93
|
+
'requestId',
|
|
94
|
+
'responseId',
|
|
95
|
+
'usage',
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
/** A failed completion. The client throws it; a runner reports its toJSON() to step. */
|
|
99
|
+
export class LlmError extends Error {
|
|
100
|
+
override readonly name = 'LlmError';
|
|
101
|
+
readonly failure: LlmFailure;
|
|
102
|
+
|
|
103
|
+
constructor(message: string, options: LlmErrorOptions) {
|
|
104
|
+
super(message);
|
|
105
|
+
const { kind, retryable, ...detail } = options;
|
|
106
|
+
const failure: { [key: string]: Json | undefined } = {
|
|
107
|
+
name: 'LlmError',
|
|
108
|
+
message,
|
|
109
|
+
kind,
|
|
110
|
+
retryable: retryable ?? RETRYABLE[kind],
|
|
111
|
+
};
|
|
112
|
+
for (const key of DETAIL) if (detail[key] !== undefined) failure[key] = detail[key];
|
|
113
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
114
|
+
this.failure = failure as LlmFailure;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get kind(): LlmFailureKind {
|
|
118
|
+
return this.failure.kind;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get retryable(): boolean {
|
|
122
|
+
return this.failure.retryable;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The failure as Json, which a runner reports as the effect's error. */
|
|
126
|
+
toJSON(): LlmFailure {
|
|
127
|
+
return this.failure;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** An LlmError from its Json, such as a gateway's answer. */
|
|
131
|
+
static fromJSON({ message, kind, retryable, ...detail }: LlmFailure): LlmError {
|
|
132
|
+
const options: LlmErrorOptions = { ...omitName(detail), kind, retryable };
|
|
133
|
+
return new LlmError(message, options);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function omitName(detail: Detail & { readonly name?: string }): Detail {
|
|
138
|
+
const { name: _name, ...rest } = detail;
|
|
139
|
+
return rest;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Whether a value is an LlmFailure, such as a failed effect's error. */
|
|
143
|
+
export function isLlmFailure(value: Json | undefined): value is LlmFailure {
|
|
144
|
+
if (!isObject(value)) return false;
|
|
145
|
+
const { name, message, kind, retryable } = value;
|
|
146
|
+
return (
|
|
147
|
+
name === 'LlmError' &&
|
|
148
|
+
typeof message === 'string' &&
|
|
149
|
+
typeof kind === 'string' &&
|
|
150
|
+
kind in RETRYABLE &&
|
|
151
|
+
typeof retryable === 'boolean'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The LLM failure in an effect's outcome, or null: for a result, for a
|
|
157
|
+
* failure the runner reported itself (a timeout, a crash) and for one thrown
|
|
158
|
+
* by something other than this package.
|
|
159
|
+
*/
|
|
160
|
+
export function llmFailure(outcome: Outcome): LlmFailure | null {
|
|
161
|
+
if (!('failed' in outcome)) return null;
|
|
162
|
+
const { error } = outcome.failed;
|
|
163
|
+
return isLlmFailure(error) ? error : null;
|
|
164
|
+
}
|
package/src/fields.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Open records of Json: fields a shape passes through without interpreting.
|
|
2
|
+
|
|
3
|
+
import type { Json } from '@acentric/types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fields merged as they are into the provider's own block for a shared shape,
|
|
7
|
+
* such as Anthropic's `cache_control` on a content part or OpenAI's `detail`
|
|
8
|
+
* on an image. The client never interprets them.
|
|
9
|
+
*/
|
|
10
|
+
export type ProviderFields = { readonly [key: string]: Json };
|
|
11
|
+
|
|
12
|
+
/** The application's own attributes on a part or message. Never sent to a provider. */
|
|
13
|
+
export type Metadata = { readonly [key: string]: Json };
|