tina4-nodejs 3.13.113 → 3.13.115
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/CLAUDE.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +173 -17
- package/packages/core/dist/index.js +173 -17
- package/packages/core/src/aiClient.ts +218 -17
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/server.ts +34 -9
- package/packages/orm/dist/index.js +173 -17
- package/types/core/src/aiClient.d.ts +91 -7
- package/types/core/src/index.d.ts +1 -1
- package/types/core/src/server.d.ts +23 -1
package/CLAUDE.md
CHANGED
|
@@ -13,13 +13,13 @@ Even if the skill text is not currently loaded, these are non-negotiable:
|
|
|
13
13
|
|
|
14
14
|
The full discipline lives in `.claude/skills/tina4-maintainer/SKILL.md`; this block is the always-on floor.
|
|
15
15
|
|
|
16
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
16
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.115)
|
|
17
17
|
|
|
18
18
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
19
19
|
|
|
20
20
|
## What This Project Is
|
|
21
21
|
|
|
22
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
22
|
+
Tina4 for Node.js/TypeScript v3.13.115 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
23
23
|
|
|
24
24
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
25
25
|
|
package/package.json
CHANGED
package/packages/cli/dist/bin.js
CHANGED
|
@@ -36727,18 +36727,24 @@ async function runGlobalMiddlewarePass(middleware, req2, res) {
|
|
|
36727
36727
|
if (!res.raw.writableEnded) res.raw.end();
|
|
36728
36728
|
return true;
|
|
36729
36729
|
}
|
|
36730
|
-
|
|
36731
|
-
const
|
|
36732
|
-
const fnStr = match.handler.toString();
|
|
36730
|
+
function resolveHandlerArgs(handler, req2, res, routeParams) {
|
|
36731
|
+
const fnStr = handler.toString();
|
|
36733
36732
|
const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
|
|
36734
36733
|
const argNames = argMatch?.[1]?.split(",").map((a) => a.trim().replace(/[:=].*/, "")) ?? [];
|
|
36735
36734
|
const filteredArgs = argNames.filter((n) => n.length > 0);
|
|
36736
|
-
if (filteredArgs.length === 0) return
|
|
36737
|
-
|
|
36735
|
+
if (filteredArgs.length === 0) return [];
|
|
36736
|
+
let unmatchedPos = 0;
|
|
36737
|
+
return filteredArgs.map((name) => {
|
|
36738
36738
|
if (name in routeParams) return routeParams[name];
|
|
36739
36739
|
if (name === "request" || name === "req") return req2;
|
|
36740
|
-
return res;
|
|
36740
|
+
if (name === "response" || name === "res") return res;
|
|
36741
|
+
return unmatchedPos++ === 0 ? req2 : res;
|
|
36741
36742
|
});
|
|
36743
|
+
}
|
|
36744
|
+
async function invokeRouteHandler(match, req2, res) {
|
|
36745
|
+
const routeParams = req2.params || {};
|
|
36746
|
+
const args = resolveHandlerArgs(match.handler, req2, res, routeParams);
|
|
36747
|
+
if (args.length === 0) return await match.handler();
|
|
36742
36748
|
return await match.handler(...args);
|
|
36743
36749
|
}
|
|
36744
36750
|
async function renderIfTemplateRoute(match, res, result) {
|
|
@@ -42043,6 +42049,8 @@ var init_aiClient = __esm({
|
|
|
42043
42049
|
Ai = class {
|
|
42044
42050
|
static chat(messages, options = {}) {
|
|
42045
42051
|
this.validateMessages(messages);
|
|
42052
|
+
if (options.tools !== void 0) this.validateTools(options.tools);
|
|
42053
|
+
if (options.toolChoice !== void 0) this.validateToolChoice(options.toolChoice);
|
|
42046
42054
|
const config = this.config("chat", options);
|
|
42047
42055
|
const body = this.chatBody(config, messages, options);
|
|
42048
42056
|
const headers = this.headers(config);
|
|
@@ -42072,15 +42080,31 @@ var init_aiClient = __esm({
|
|
|
42072
42080
|
}
|
|
42073
42081
|
/**
|
|
42074
42082
|
* Validate role + content shape. Content may be a string OR a non-empty
|
|
42075
|
-
* list of {type:'text'|'image', ...} parts (ADR-0060
|
|
42076
|
-
*
|
|
42083
|
+
* list of {type:'text'|'image'|'tool_result', ...} parts (ADR-0060 +
|
|
42084
|
+
* ADR-0061). The `tool` role is the OpenAI-style tool-result turn
|
|
42085
|
+
* (ADR-0061). Malformed parts fail fast with AiConfigError, never
|
|
42086
|
+
* reaching the wire.
|
|
42077
42087
|
*/
|
|
42078
42088
|
static validateMessages(messages) {
|
|
42079
42089
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
42080
42090
|
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
42081
42091
|
}
|
|
42082
|
-
for (const
|
|
42083
|
-
if (!
|
|
42092
|
+
for (const raw of messages) {
|
|
42093
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
42094
|
+
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
42095
|
+
}
|
|
42096
|
+
const message = raw;
|
|
42097
|
+
const role = message.role;
|
|
42098
|
+
if (role === "tool") {
|
|
42099
|
+
if (typeof message.tool_call_id !== "string" || message.tool_call_id.length === 0) {
|
|
42100
|
+
throw new AiConfigError("AI tool message requires a non-empty string 'tool_call_id'");
|
|
42101
|
+
}
|
|
42102
|
+
if (typeof message.content !== "string") {
|
|
42103
|
+
throw new AiConfigError("AI tool message requires a string 'content'");
|
|
42104
|
+
}
|
|
42105
|
+
continue;
|
|
42106
|
+
}
|
|
42107
|
+
if (role !== "system" && role !== "user" && role !== "assistant") {
|
|
42084
42108
|
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
42085
42109
|
}
|
|
42086
42110
|
this.validateContent(message.content);
|
|
@@ -42111,11 +42135,64 @@ var init_aiClient = __esm({
|
|
|
42111
42135
|
if (record.source.startsWith("data:") && !/^data:[^;,\s]+;base64,[A-Za-z0-9+/=]+$/.test(record.source)) {
|
|
42112
42136
|
throw new AiConfigError("AI image data URI must be data:<media_type>;base64,<payload>");
|
|
42113
42137
|
}
|
|
42138
|
+
} else if (partType === "tool_result") {
|
|
42139
|
+
if (typeof record.tool_use_id !== "string" || record.tool_use_id.length === 0) {
|
|
42140
|
+
throw new AiConfigError("AI tool_result part requires a non-empty string 'tool_use_id'");
|
|
42141
|
+
}
|
|
42142
|
+
if (typeof record.content !== "string") {
|
|
42143
|
+
throw new AiConfigError("AI tool_result part requires a string 'content'");
|
|
42144
|
+
}
|
|
42114
42145
|
} else {
|
|
42115
42146
|
throw new AiConfigError(`AI content part has unknown type '${String(partType)}'`);
|
|
42116
42147
|
}
|
|
42117
42148
|
}
|
|
42118
42149
|
}
|
|
42150
|
+
/**
|
|
42151
|
+
* Validate the outbound tool declarations (ADR-0061). Each tool needs a
|
|
42152
|
+
* non-empty `name`, a string `description`, and a JSON-Schema-shaped
|
|
42153
|
+
* `parameters` object. Malformed tools fail fast with AiConfigError,
|
|
42154
|
+
* never reaching the wire.
|
|
42155
|
+
*/
|
|
42156
|
+
static validateTools(tools) {
|
|
42157
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
42158
|
+
throw new AiConfigError("AI tools must be a non-empty list of {name, description, parameters}");
|
|
42159
|
+
}
|
|
42160
|
+
for (const tool of tools) {
|
|
42161
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
|
|
42162
|
+
throw new AiConfigError("AI tool must be an object with name, description, parameters");
|
|
42163
|
+
}
|
|
42164
|
+
const record = tool;
|
|
42165
|
+
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
42166
|
+
throw new AiConfigError("AI tool requires a non-empty string 'name'");
|
|
42167
|
+
}
|
|
42168
|
+
if (typeof record.description !== "string") {
|
|
42169
|
+
throw new AiConfigError("AI tool requires a string 'description'");
|
|
42170
|
+
}
|
|
42171
|
+
if (!record.parameters || typeof record.parameters !== "object" || Array.isArray(record.parameters)) {
|
|
42172
|
+
throw new AiConfigError("AI tool requires a JSON-Schema object 'parameters'");
|
|
42173
|
+
}
|
|
42174
|
+
}
|
|
42175
|
+
}
|
|
42176
|
+
/**
|
|
42177
|
+
* Validate the outbound tool_choice value (ADR-0061). The four accepted
|
|
42178
|
+
* shapes are 'auto', 'none', 'required', and {name: 'x'}.
|
|
42179
|
+
*/
|
|
42180
|
+
static validateToolChoice(choice) {
|
|
42181
|
+
if (typeof choice === "string") {
|
|
42182
|
+
if (choice !== "auto" && choice !== "none" && choice !== "required") {
|
|
42183
|
+
throw new AiConfigError("AI toolChoice string must be 'auto', 'none', or 'required'");
|
|
42184
|
+
}
|
|
42185
|
+
return;
|
|
42186
|
+
}
|
|
42187
|
+
if (choice && typeof choice === "object" && !Array.isArray(choice)) {
|
|
42188
|
+
const record = choice;
|
|
42189
|
+
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
42190
|
+
throw new AiConfigError("AI toolChoice object requires a non-empty string 'name'");
|
|
42191
|
+
}
|
|
42192
|
+
return;
|
|
42193
|
+
}
|
|
42194
|
+
throw new AiConfigError("AI toolChoice must be 'auto'|'none'|'required' or {name: string}");
|
|
42195
|
+
}
|
|
42119
42196
|
static number(name, fallback, minimum) {
|
|
42120
42197
|
const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
|
|
42121
42198
|
if (!Number.isFinite(value) || value < minimum) throw new AiConfigError(`${name} must be numeric and at least ${minimum}`);
|
|
@@ -42164,25 +42241,100 @@ var init_aiClient = __esm({
|
|
|
42164
42241
|
}
|
|
42165
42242
|
/**
|
|
42166
42243
|
* Build the provider-specific request body from a Tina4-shaped message
|
|
42167
|
-
* list
|
|
42168
|
-
*
|
|
42169
|
-
*
|
|
42244
|
+
* list plus optional tool declarations (ADR-0060 + ADR-0061).
|
|
42245
|
+
*
|
|
42246
|
+
* Content parts translate per provider:
|
|
42247
|
+
* - OpenAI/local: image → {type:'image_url', image_url:{url}}
|
|
42248
|
+
* - Anthropic: image → {type:'image', source:{type:'base64'|'url', ...}}
|
|
42170
42249
|
* String content is preserved verbatim in the OpenAI/local shape and
|
|
42171
42250
|
* likewise for Anthropic (both accept a bare string).
|
|
42251
|
+
*
|
|
42252
|
+
* Tool-result turns are normalised to the current provider's expected
|
|
42253
|
+
* shape (either the OpenAI `{role:"tool", tool_call_id, content}` turn or
|
|
42254
|
+
* the Anthropic `{role:"user", content:[{type:"tool_result", ...}]}`
|
|
42255
|
+
* turn), so an agent-loop written against Tina4 never has to fork on
|
|
42256
|
+
* TINA4_AI_PROVIDER (ADR-0061 wire translation).
|
|
42172
42257
|
*/
|
|
42173
42258
|
static chatBody(config, messages, options) {
|
|
42174
|
-
const
|
|
42175
|
-
const body = { model: config.model, messages:
|
|
42259
|
+
const normalized = this.normalizeMessagesForProvider(messages, config.provider);
|
|
42260
|
+
const body = { model: config.model, messages: normalized, stream: options.stream ?? false };
|
|
42176
42261
|
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
42177
42262
|
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
42178
42263
|
if (config.provider === "anthropic") {
|
|
42179
|
-
const systemParts =
|
|
42180
|
-
|
|
42264
|
+
const systemParts = [];
|
|
42265
|
+
for (const message of messages) {
|
|
42266
|
+
if (message.role !== "system") continue;
|
|
42267
|
+
const content = message.content;
|
|
42268
|
+
systemParts.push(typeof content === "string" ? content : this.contentToPlainText(content));
|
|
42269
|
+
}
|
|
42270
|
+
body.messages = normalized.filter((message) => message.role !== "system");
|
|
42181
42271
|
body.max_tokens = options.maxTokens ?? 1024;
|
|
42182
42272
|
if (systemParts.length) body.system = systemParts.join("\n\n");
|
|
42183
42273
|
}
|
|
42274
|
+
this.applyTools(body, config.provider, options);
|
|
42184
42275
|
return body;
|
|
42185
42276
|
}
|
|
42277
|
+
/**
|
|
42278
|
+
* Normalise the Tina4-shaped messages into the provider's on-wire shape.
|
|
42279
|
+
* The `tool` role and the `tool_result` content part are translated
|
|
42280
|
+
* between the OpenAI and Anthropic forms so either input works against
|
|
42281
|
+
* either provider (ADR-0061 return-path table).
|
|
42282
|
+
*/
|
|
42283
|
+
static normalizeMessagesForProvider(messages, provider) {
|
|
42284
|
+
const out = [];
|
|
42285
|
+
for (const message of messages) {
|
|
42286
|
+
if (message.role === "tool") {
|
|
42287
|
+
if (provider === "anthropic") {
|
|
42288
|
+
out.push({
|
|
42289
|
+
role: "user",
|
|
42290
|
+
content: [{ type: "tool_result", tool_use_id: message.tool_call_id, content: message.content }]
|
|
42291
|
+
});
|
|
42292
|
+
} else {
|
|
42293
|
+
out.push({ role: "tool", tool_call_id: message.tool_call_id, content: message.content });
|
|
42294
|
+
}
|
|
42295
|
+
continue;
|
|
42296
|
+
}
|
|
42297
|
+
if (Array.isArray(message.content) && message.content.some((part) => part.type === "tool_result")) {
|
|
42298
|
+
if (provider === "anthropic") {
|
|
42299
|
+
out.push({ role: message.role, content: this.translateContent(message.content, provider) });
|
|
42300
|
+
} else {
|
|
42301
|
+
for (const part of message.content) {
|
|
42302
|
+
if (part.type === "tool_result") {
|
|
42303
|
+
out.push({ role: "tool", tool_call_id: part.tool_use_id, content: part.content });
|
|
42304
|
+
}
|
|
42305
|
+
}
|
|
42306
|
+
}
|
|
42307
|
+
continue;
|
|
42308
|
+
}
|
|
42309
|
+
out.push({ role: message.role, content: this.translateContent(message.content, provider) });
|
|
42310
|
+
}
|
|
42311
|
+
return out;
|
|
42312
|
+
}
|
|
42313
|
+
/**
|
|
42314
|
+
* Attach the outbound `tools` and `tool_choice` (ADR-0061 outbound
|
|
42315
|
+
* translation tables) to the body in place. When toolChoice is 'none'
|
|
42316
|
+
* on Anthropic (Anthropic has no "none" mode) the tools list is omitted
|
|
42317
|
+
* entirely — the model cannot call what it cannot see.
|
|
42318
|
+
*/
|
|
42319
|
+
static applyTools(body, provider, options) {
|
|
42320
|
+
const choice = options.toolChoice;
|
|
42321
|
+
const suppressToolsForAnthropic = provider === "anthropic" && choice === "none";
|
|
42322
|
+
if (options.tools !== void 0 && !suppressToolsForAnthropic) {
|
|
42323
|
+
body.tools = options.tools.map(
|
|
42324
|
+
(tool) => provider === "anthropic" ? { name: tool.name, description: tool.description, input_schema: tool.parameters } : { type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } }
|
|
42325
|
+
);
|
|
42326
|
+
}
|
|
42327
|
+
if (choice === void 0) return;
|
|
42328
|
+
if (provider === "anthropic") {
|
|
42329
|
+
if (choice === "none") return;
|
|
42330
|
+
if (choice === "auto") body.tool_choice = { type: "auto" };
|
|
42331
|
+
else if (choice === "required") body.tool_choice = { type: "any" };
|
|
42332
|
+
else body.tool_choice = { type: "tool", name: choice.name };
|
|
42333
|
+
} else {
|
|
42334
|
+
if (typeof choice === "string") body.tool_choice = choice;
|
|
42335
|
+
else body.tool_choice = { type: "function", function: { name: choice.name } };
|
|
42336
|
+
}
|
|
42337
|
+
}
|
|
42186
42338
|
/**
|
|
42187
42339
|
* Translate one message content value into the provider's on-wire shape.
|
|
42188
42340
|
* A plain string is passed through (both providers accept a string
|
|
@@ -42193,6 +42345,7 @@ var init_aiClient = __esm({
|
|
|
42193
42345
|
if (provider === "anthropic") {
|
|
42194
42346
|
return content.map((part) => {
|
|
42195
42347
|
if (part.type === "text") return { type: "text", text: part.text };
|
|
42348
|
+
if (part.type === "tool_result") return { type: "tool_result", tool_use_id: part.tool_use_id, content: part.content };
|
|
42196
42349
|
if (part.source.startsWith("data:")) {
|
|
42197
42350
|
const parsed = this.parseDataUri(part.source);
|
|
42198
42351
|
return { type: "image", source: { type: "base64", media_type: parsed.mediaType, data: parsed.data } };
|
|
@@ -42202,6 +42355,9 @@ var init_aiClient = __esm({
|
|
|
42202
42355
|
}
|
|
42203
42356
|
return content.map((part) => {
|
|
42204
42357
|
if (part.type === "text") return { type: "text", text: part.text };
|
|
42358
|
+
if (part.type === "tool_result") {
|
|
42359
|
+
return { type: "text", text: part.content };
|
|
42360
|
+
}
|
|
42205
42361
|
return { type: "image_url", image_url: { url: part.source } };
|
|
42206
42362
|
});
|
|
42207
42363
|
}
|
|
@@ -36706,18 +36706,24 @@ async function runGlobalMiddlewarePass(middleware, req2, res) {
|
|
|
36706
36706
|
if (!res.raw.writableEnded) res.raw.end();
|
|
36707
36707
|
return true;
|
|
36708
36708
|
}
|
|
36709
|
-
|
|
36710
|
-
const
|
|
36711
|
-
const fnStr = match.handler.toString();
|
|
36709
|
+
function resolveHandlerArgs(handler, req2, res, routeParams) {
|
|
36710
|
+
const fnStr = handler.toString();
|
|
36712
36711
|
const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
|
|
36713
36712
|
const argNames = argMatch?.[1]?.split(",").map((a) => a.trim().replace(/[:=].*/, "")) ?? [];
|
|
36714
36713
|
const filteredArgs = argNames.filter((n) => n.length > 0);
|
|
36715
|
-
if (filteredArgs.length === 0) return
|
|
36716
|
-
|
|
36714
|
+
if (filteredArgs.length === 0) return [];
|
|
36715
|
+
let unmatchedPos = 0;
|
|
36716
|
+
return filteredArgs.map((name) => {
|
|
36717
36717
|
if (name in routeParams) return routeParams[name];
|
|
36718
36718
|
if (name === "request" || name === "req") return req2;
|
|
36719
|
-
return res;
|
|
36719
|
+
if (name === "response" || name === "res") return res;
|
|
36720
|
+
return unmatchedPos++ === 0 ? req2 : res;
|
|
36720
36721
|
});
|
|
36722
|
+
}
|
|
36723
|
+
async function invokeRouteHandler(match, req2, res) {
|
|
36724
|
+
const routeParams = req2.params || {};
|
|
36725
|
+
const args = resolveHandlerArgs(match.handler, req2, res, routeParams);
|
|
36726
|
+
if (args.length === 0) return await match.handler();
|
|
36721
36727
|
return await match.handler(...args);
|
|
36722
36728
|
}
|
|
36723
36729
|
async function renderIfTemplateRoute(match, res, result) {
|
|
@@ -42004,6 +42010,8 @@ var init_aiClient = __esm({
|
|
|
42004
42010
|
Ai = class {
|
|
42005
42011
|
static chat(messages, options = {}) {
|
|
42006
42012
|
this.validateMessages(messages);
|
|
42013
|
+
if (options.tools !== void 0) this.validateTools(options.tools);
|
|
42014
|
+
if (options.toolChoice !== void 0) this.validateToolChoice(options.toolChoice);
|
|
42007
42015
|
const config = this.config("chat", options);
|
|
42008
42016
|
const body = this.chatBody(config, messages, options);
|
|
42009
42017
|
const headers = this.headers(config);
|
|
@@ -42033,15 +42041,31 @@ var init_aiClient = __esm({
|
|
|
42033
42041
|
}
|
|
42034
42042
|
/**
|
|
42035
42043
|
* Validate role + content shape. Content may be a string OR a non-empty
|
|
42036
|
-
* list of {type:'text'|'image', ...} parts (ADR-0060
|
|
42037
|
-
*
|
|
42044
|
+
* list of {type:'text'|'image'|'tool_result', ...} parts (ADR-0060 +
|
|
42045
|
+
* ADR-0061). The `tool` role is the OpenAI-style tool-result turn
|
|
42046
|
+
* (ADR-0061). Malformed parts fail fast with AiConfigError, never
|
|
42047
|
+
* reaching the wire.
|
|
42038
42048
|
*/
|
|
42039
42049
|
static validateMessages(messages) {
|
|
42040
42050
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
42041
42051
|
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
42042
42052
|
}
|
|
42043
|
-
for (const
|
|
42044
|
-
if (!
|
|
42053
|
+
for (const raw of messages) {
|
|
42054
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
42055
|
+
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
42056
|
+
}
|
|
42057
|
+
const message = raw;
|
|
42058
|
+
const role = message.role;
|
|
42059
|
+
if (role === "tool") {
|
|
42060
|
+
if (typeof message.tool_call_id !== "string" || message.tool_call_id.length === 0) {
|
|
42061
|
+
throw new AiConfigError("AI tool message requires a non-empty string 'tool_call_id'");
|
|
42062
|
+
}
|
|
42063
|
+
if (typeof message.content !== "string") {
|
|
42064
|
+
throw new AiConfigError("AI tool message requires a string 'content'");
|
|
42065
|
+
}
|
|
42066
|
+
continue;
|
|
42067
|
+
}
|
|
42068
|
+
if (role !== "system" && role !== "user" && role !== "assistant") {
|
|
42045
42069
|
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
42046
42070
|
}
|
|
42047
42071
|
this.validateContent(message.content);
|
|
@@ -42072,11 +42096,64 @@ var init_aiClient = __esm({
|
|
|
42072
42096
|
if (record.source.startsWith("data:") && !/^data:[^;,\s]+;base64,[A-Za-z0-9+/=]+$/.test(record.source)) {
|
|
42073
42097
|
throw new AiConfigError("AI image data URI must be data:<media_type>;base64,<payload>");
|
|
42074
42098
|
}
|
|
42099
|
+
} else if (partType === "tool_result") {
|
|
42100
|
+
if (typeof record.tool_use_id !== "string" || record.tool_use_id.length === 0) {
|
|
42101
|
+
throw new AiConfigError("AI tool_result part requires a non-empty string 'tool_use_id'");
|
|
42102
|
+
}
|
|
42103
|
+
if (typeof record.content !== "string") {
|
|
42104
|
+
throw new AiConfigError("AI tool_result part requires a string 'content'");
|
|
42105
|
+
}
|
|
42075
42106
|
} else {
|
|
42076
42107
|
throw new AiConfigError(`AI content part has unknown type '${String(partType)}'`);
|
|
42077
42108
|
}
|
|
42078
42109
|
}
|
|
42079
42110
|
}
|
|
42111
|
+
/**
|
|
42112
|
+
* Validate the outbound tool declarations (ADR-0061). Each tool needs a
|
|
42113
|
+
* non-empty `name`, a string `description`, and a JSON-Schema-shaped
|
|
42114
|
+
* `parameters` object. Malformed tools fail fast with AiConfigError,
|
|
42115
|
+
* never reaching the wire.
|
|
42116
|
+
*/
|
|
42117
|
+
static validateTools(tools) {
|
|
42118
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
42119
|
+
throw new AiConfigError("AI tools must be a non-empty list of {name, description, parameters}");
|
|
42120
|
+
}
|
|
42121
|
+
for (const tool of tools) {
|
|
42122
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
|
|
42123
|
+
throw new AiConfigError("AI tool must be an object with name, description, parameters");
|
|
42124
|
+
}
|
|
42125
|
+
const record = tool;
|
|
42126
|
+
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
42127
|
+
throw new AiConfigError("AI tool requires a non-empty string 'name'");
|
|
42128
|
+
}
|
|
42129
|
+
if (typeof record.description !== "string") {
|
|
42130
|
+
throw new AiConfigError("AI tool requires a string 'description'");
|
|
42131
|
+
}
|
|
42132
|
+
if (!record.parameters || typeof record.parameters !== "object" || Array.isArray(record.parameters)) {
|
|
42133
|
+
throw new AiConfigError("AI tool requires a JSON-Schema object 'parameters'");
|
|
42134
|
+
}
|
|
42135
|
+
}
|
|
42136
|
+
}
|
|
42137
|
+
/**
|
|
42138
|
+
* Validate the outbound tool_choice value (ADR-0061). The four accepted
|
|
42139
|
+
* shapes are 'auto', 'none', 'required', and {name: 'x'}.
|
|
42140
|
+
*/
|
|
42141
|
+
static validateToolChoice(choice) {
|
|
42142
|
+
if (typeof choice === "string") {
|
|
42143
|
+
if (choice !== "auto" && choice !== "none" && choice !== "required") {
|
|
42144
|
+
throw new AiConfigError("AI toolChoice string must be 'auto', 'none', or 'required'");
|
|
42145
|
+
}
|
|
42146
|
+
return;
|
|
42147
|
+
}
|
|
42148
|
+
if (choice && typeof choice === "object" && !Array.isArray(choice)) {
|
|
42149
|
+
const record = choice;
|
|
42150
|
+
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
42151
|
+
throw new AiConfigError("AI toolChoice object requires a non-empty string 'name'");
|
|
42152
|
+
}
|
|
42153
|
+
return;
|
|
42154
|
+
}
|
|
42155
|
+
throw new AiConfigError("AI toolChoice must be 'auto'|'none'|'required' or {name: string}");
|
|
42156
|
+
}
|
|
42080
42157
|
static number(name, fallback, minimum) {
|
|
42081
42158
|
const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
|
|
42082
42159
|
if (!Number.isFinite(value) || value < minimum) throw new AiConfigError(`${name} must be numeric and at least ${minimum}`);
|
|
@@ -42125,25 +42202,100 @@ var init_aiClient = __esm({
|
|
|
42125
42202
|
}
|
|
42126
42203
|
/**
|
|
42127
42204
|
* Build the provider-specific request body from a Tina4-shaped message
|
|
42128
|
-
* list
|
|
42129
|
-
*
|
|
42130
|
-
*
|
|
42205
|
+
* list plus optional tool declarations (ADR-0060 + ADR-0061).
|
|
42206
|
+
*
|
|
42207
|
+
* Content parts translate per provider:
|
|
42208
|
+
* - OpenAI/local: image → {type:'image_url', image_url:{url}}
|
|
42209
|
+
* - Anthropic: image → {type:'image', source:{type:'base64'|'url', ...}}
|
|
42131
42210
|
* String content is preserved verbatim in the OpenAI/local shape and
|
|
42132
42211
|
* likewise for Anthropic (both accept a bare string).
|
|
42212
|
+
*
|
|
42213
|
+
* Tool-result turns are normalised to the current provider's expected
|
|
42214
|
+
* shape (either the OpenAI `{role:"tool", tool_call_id, content}` turn or
|
|
42215
|
+
* the Anthropic `{role:"user", content:[{type:"tool_result", ...}]}`
|
|
42216
|
+
* turn), so an agent-loop written against Tina4 never has to fork on
|
|
42217
|
+
* TINA4_AI_PROVIDER (ADR-0061 wire translation).
|
|
42133
42218
|
*/
|
|
42134
42219
|
static chatBody(config, messages, options) {
|
|
42135
|
-
const
|
|
42136
|
-
const body = { model: config.model, messages:
|
|
42220
|
+
const normalized = this.normalizeMessagesForProvider(messages, config.provider);
|
|
42221
|
+
const body = { model: config.model, messages: normalized, stream: options.stream ?? false };
|
|
42137
42222
|
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
42138
42223
|
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
42139
42224
|
if (config.provider === "anthropic") {
|
|
42140
|
-
const systemParts =
|
|
42141
|
-
|
|
42225
|
+
const systemParts = [];
|
|
42226
|
+
for (const message of messages) {
|
|
42227
|
+
if (message.role !== "system") continue;
|
|
42228
|
+
const content = message.content;
|
|
42229
|
+
systemParts.push(typeof content === "string" ? content : this.contentToPlainText(content));
|
|
42230
|
+
}
|
|
42231
|
+
body.messages = normalized.filter((message) => message.role !== "system");
|
|
42142
42232
|
body.max_tokens = options.maxTokens ?? 1024;
|
|
42143
42233
|
if (systemParts.length) body.system = systemParts.join("\n\n");
|
|
42144
42234
|
}
|
|
42235
|
+
this.applyTools(body, config.provider, options);
|
|
42145
42236
|
return body;
|
|
42146
42237
|
}
|
|
42238
|
+
/**
|
|
42239
|
+
* Normalise the Tina4-shaped messages into the provider's on-wire shape.
|
|
42240
|
+
* The `tool` role and the `tool_result` content part are translated
|
|
42241
|
+
* between the OpenAI and Anthropic forms so either input works against
|
|
42242
|
+
* either provider (ADR-0061 return-path table).
|
|
42243
|
+
*/
|
|
42244
|
+
static normalizeMessagesForProvider(messages, provider) {
|
|
42245
|
+
const out = [];
|
|
42246
|
+
for (const message of messages) {
|
|
42247
|
+
if (message.role === "tool") {
|
|
42248
|
+
if (provider === "anthropic") {
|
|
42249
|
+
out.push({
|
|
42250
|
+
role: "user",
|
|
42251
|
+
content: [{ type: "tool_result", tool_use_id: message.tool_call_id, content: message.content }]
|
|
42252
|
+
});
|
|
42253
|
+
} else {
|
|
42254
|
+
out.push({ role: "tool", tool_call_id: message.tool_call_id, content: message.content });
|
|
42255
|
+
}
|
|
42256
|
+
continue;
|
|
42257
|
+
}
|
|
42258
|
+
if (Array.isArray(message.content) && message.content.some((part) => part.type === "tool_result")) {
|
|
42259
|
+
if (provider === "anthropic") {
|
|
42260
|
+
out.push({ role: message.role, content: this.translateContent(message.content, provider) });
|
|
42261
|
+
} else {
|
|
42262
|
+
for (const part of message.content) {
|
|
42263
|
+
if (part.type === "tool_result") {
|
|
42264
|
+
out.push({ role: "tool", tool_call_id: part.tool_use_id, content: part.content });
|
|
42265
|
+
}
|
|
42266
|
+
}
|
|
42267
|
+
}
|
|
42268
|
+
continue;
|
|
42269
|
+
}
|
|
42270
|
+
out.push({ role: message.role, content: this.translateContent(message.content, provider) });
|
|
42271
|
+
}
|
|
42272
|
+
return out;
|
|
42273
|
+
}
|
|
42274
|
+
/**
|
|
42275
|
+
* Attach the outbound `tools` and `tool_choice` (ADR-0061 outbound
|
|
42276
|
+
* translation tables) to the body in place. When toolChoice is 'none'
|
|
42277
|
+
* on Anthropic (Anthropic has no "none" mode) the tools list is omitted
|
|
42278
|
+
* entirely — the model cannot call what it cannot see.
|
|
42279
|
+
*/
|
|
42280
|
+
static applyTools(body, provider, options) {
|
|
42281
|
+
const choice = options.toolChoice;
|
|
42282
|
+
const suppressToolsForAnthropic = provider === "anthropic" && choice === "none";
|
|
42283
|
+
if (options.tools !== void 0 && !suppressToolsForAnthropic) {
|
|
42284
|
+
body.tools = options.tools.map(
|
|
42285
|
+
(tool) => provider === "anthropic" ? { name: tool.name, description: tool.description, input_schema: tool.parameters } : { type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } }
|
|
42286
|
+
);
|
|
42287
|
+
}
|
|
42288
|
+
if (choice === void 0) return;
|
|
42289
|
+
if (provider === "anthropic") {
|
|
42290
|
+
if (choice === "none") return;
|
|
42291
|
+
if (choice === "auto") body.tool_choice = { type: "auto" };
|
|
42292
|
+
else if (choice === "required") body.tool_choice = { type: "any" };
|
|
42293
|
+
else body.tool_choice = { type: "tool", name: choice.name };
|
|
42294
|
+
} else {
|
|
42295
|
+
if (typeof choice === "string") body.tool_choice = choice;
|
|
42296
|
+
else body.tool_choice = { type: "function", function: { name: choice.name } };
|
|
42297
|
+
}
|
|
42298
|
+
}
|
|
42147
42299
|
/**
|
|
42148
42300
|
* Translate one message content value into the provider's on-wire shape.
|
|
42149
42301
|
* A plain string is passed through (both providers accept a string
|
|
@@ -42154,6 +42306,7 @@ var init_aiClient = __esm({
|
|
|
42154
42306
|
if (provider === "anthropic") {
|
|
42155
42307
|
return content.map((part) => {
|
|
42156
42308
|
if (part.type === "text") return { type: "text", text: part.text };
|
|
42309
|
+
if (part.type === "tool_result") return { type: "tool_result", tool_use_id: part.tool_use_id, content: part.content };
|
|
42157
42310
|
if (part.source.startsWith("data:")) {
|
|
42158
42311
|
const parsed = this.parseDataUri(part.source);
|
|
42159
42312
|
return { type: "image", source: { type: "base64", media_type: parsed.mediaType, data: parsed.data } };
|
|
@@ -42163,6 +42316,9 @@ var init_aiClient = __esm({
|
|
|
42163
42316
|
}
|
|
42164
42317
|
return content.map((part) => {
|
|
42165
42318
|
if (part.type === "text") return { type: "text", text: part.text };
|
|
42319
|
+
if (part.type === "tool_result") {
|
|
42320
|
+
return { type: "text", text: part.content };
|
|
42321
|
+
}
|
|
42166
42322
|
return { type: "image_url", image_url: { url: part.source } };
|
|
42167
42323
|
});
|
|
42168
42324
|
}
|