tina4-nodejs 3.13.135 → 3.13.137
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 +37 -8
- package/packages/core/dist/index.js +37 -8
- package/packages/core/public/swagger/index.html +1 -1
- package/packages/core/src/aiClient.ts +14 -7
- package/packages/orm/dist/index.js +37 -8
- package/packages/orm/src/migration.ts +13 -0
- package/packages/swagger/dist/index.js +27 -3
- package/packages/swagger/src/ui.ts +28 -3
- package/types/core/src/aiClient.d.ts +2 -2
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.137)
|
|
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.137 - 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
|
@@ -12388,6 +12388,10 @@ async function recordApplied(db, name, batch, passed = 1) {
|
|
|
12388
12388
|
insertCols.push("migration_id");
|
|
12389
12389
|
values.push(name);
|
|
12390
12390
|
}
|
|
12391
|
+
if (cols.has("name")) {
|
|
12392
|
+
insertCols.push("name");
|
|
12393
|
+
values.push(name);
|
|
12394
|
+
}
|
|
12391
12395
|
if (isFirebirdAdapter(db)) {
|
|
12392
12396
|
const rows = await adapterQuery(
|
|
12393
12397
|
db,
|
|
@@ -37955,13 +37959,37 @@ function swaggerEnabled() {
|
|
|
37955
37959
|
return ["true", "1", "yes", "on"].includes(raw);
|
|
37956
37960
|
}
|
|
37957
37961
|
function createSwaggerRoutes(getSpec) {
|
|
37962
|
+
const serveUi = async (_req, res) => {
|
|
37963
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
37964
|
+
};
|
|
37958
37965
|
return [
|
|
37959
37966
|
{
|
|
37960
37967
|
method: "GET",
|
|
37961
37968
|
pattern: "/swagger",
|
|
37962
|
-
handler:
|
|
37963
|
-
|
|
37964
|
-
|
|
37969
|
+
handler: serveUi
|
|
37970
|
+
},
|
|
37971
|
+
{
|
|
37972
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
37973
|
+
//
|
|
37974
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
37975
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
37976
|
+
// this route and was answered by the framework-bundled
|
|
37977
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
37978
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
37979
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
37980
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
37981
|
+
// cdnjs, while the page this handler renders loads from
|
|
37982
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
37983
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
37984
|
+
//
|
|
37985
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
37986
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
37987
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
37988
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
37989
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
37990
|
+
method: "GET",
|
|
37991
|
+
pattern: "/swagger/",
|
|
37992
|
+
handler: serveUi
|
|
37965
37993
|
},
|
|
37966
37994
|
{
|
|
37967
37995
|
method: "GET",
|
|
@@ -44682,13 +44710,14 @@ var init_aiClient = __esm({
|
|
|
44682
44710
|
}
|
|
44683
44711
|
static config(capability, options) {
|
|
44684
44712
|
const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
|
|
44685
|
-
if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or
|
|
44713
|
+
if (provider !== "local" && provider !== "openai" && provider !== "anthropic" && provider !== "gemini") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, anthropic, or gemini");
|
|
44686
44714
|
const key = process.env.TINA4_AI_KEY || null;
|
|
44687
|
-
if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
44715
|
+
if ((provider === "openai" || provider === "anthropic" || provider === "gemini") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
44688
44716
|
const defaults = {
|
|
44689
44717
|
local: ["http://localhost:11437", "llama3.2"],
|
|
44690
44718
|
openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
|
|
44691
|
-
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
|
|
44719
|
+
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"],
|
|
44720
|
+
gemini: ["https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.5-flash"]
|
|
44692
44721
|
};
|
|
44693
44722
|
const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
|
|
44694
44723
|
const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
|
|
@@ -44706,7 +44735,7 @@ var init_aiClient = __esm({
|
|
|
44706
44735
|
}
|
|
44707
44736
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
|
|
44708
44737
|
const path10 = url.pathname.replace(/\/+$/, "");
|
|
44709
|
-
if (path10 === "" || path10 === "/v1" || path10 === "/api") {
|
|
44738
|
+
if (path10 === "" || path10 === "/v1" || path10 === "/api" || path10 === "/v1beta/openai") {
|
|
44710
44739
|
const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
|
|
44711
44740
|
url.pathname = (path10 || "/v1") + suffix;
|
|
44712
44741
|
}
|
|
@@ -44714,7 +44743,7 @@ var init_aiClient = __esm({
|
|
|
44714
44743
|
}
|
|
44715
44744
|
static headers(config) {
|
|
44716
44745
|
const headers = { "content-type": "application/json", accept: "application/json" };
|
|
44717
|
-
if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
|
|
44746
|
+
if (config.provider === "openai" || config.provider === "gemini") headers.authorization = `Bearer ${config.key}`;
|
|
44718
44747
|
if (config.provider === "anthropic") {
|
|
44719
44748
|
headers["x-api-key"] = config.key;
|
|
44720
44749
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -12387,6 +12387,10 @@ async function recordApplied(db, name, batch, passed = 1) {
|
|
|
12387
12387
|
insertCols.push("migration_id");
|
|
12388
12388
|
values.push(name);
|
|
12389
12389
|
}
|
|
12390
|
+
if (cols.has("name")) {
|
|
12391
|
+
insertCols.push("name");
|
|
12392
|
+
values.push(name);
|
|
12393
|
+
}
|
|
12390
12394
|
if (isFirebirdAdapter(db)) {
|
|
12391
12395
|
const rows = await adapterQuery(
|
|
12392
12396
|
db,
|
|
@@ -37934,13 +37938,37 @@ function swaggerEnabled() {
|
|
|
37934
37938
|
return ["true", "1", "yes", "on"].includes(raw);
|
|
37935
37939
|
}
|
|
37936
37940
|
function createSwaggerRoutes(getSpec) {
|
|
37941
|
+
const serveUi = async (_req, res) => {
|
|
37942
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
37943
|
+
};
|
|
37937
37944
|
return [
|
|
37938
37945
|
{
|
|
37939
37946
|
method: "GET",
|
|
37940
37947
|
pattern: "/swagger",
|
|
37941
|
-
handler:
|
|
37942
|
-
|
|
37943
|
-
|
|
37948
|
+
handler: serveUi
|
|
37949
|
+
},
|
|
37950
|
+
{
|
|
37951
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
37952
|
+
//
|
|
37953
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
37954
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
37955
|
+
// this route and was answered by the framework-bundled
|
|
37956
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
37957
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
37958
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
37959
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
37960
|
+
// cdnjs, while the page this handler renders loads from
|
|
37961
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
37962
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
37963
|
+
//
|
|
37964
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
37965
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
37966
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
37967
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
37968
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
37969
|
+
method: "GET",
|
|
37970
|
+
pattern: "/swagger/",
|
|
37971
|
+
handler: serveUi
|
|
37944
37972
|
},
|
|
37945
37973
|
{
|
|
37946
37974
|
method: "GET",
|
|
@@ -44643,13 +44671,14 @@ var init_aiClient = __esm({
|
|
|
44643
44671
|
}
|
|
44644
44672
|
static config(capability, options) {
|
|
44645
44673
|
const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
|
|
44646
|
-
if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or
|
|
44674
|
+
if (provider !== "local" && provider !== "openai" && provider !== "anthropic" && provider !== "gemini") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, anthropic, or gemini");
|
|
44647
44675
|
const key = process.env.TINA4_AI_KEY || null;
|
|
44648
|
-
if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
44676
|
+
if ((provider === "openai" || provider === "anthropic" || provider === "gemini") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
44649
44677
|
const defaults = {
|
|
44650
44678
|
local: ["http://localhost:11437", "llama3.2"],
|
|
44651
44679
|
openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
|
|
44652
|
-
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
|
|
44680
|
+
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"],
|
|
44681
|
+
gemini: ["https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.5-flash"]
|
|
44653
44682
|
};
|
|
44654
44683
|
const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
|
|
44655
44684
|
const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
|
|
@@ -44667,7 +44696,7 @@ var init_aiClient = __esm({
|
|
|
44667
44696
|
}
|
|
44668
44697
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
|
|
44669
44698
|
const path10 = url.pathname.replace(/\/+$/, "");
|
|
44670
|
-
if (path10 === "" || path10 === "/v1" || path10 === "/api") {
|
|
44699
|
+
if (path10 === "" || path10 === "/v1" || path10 === "/api" || path10 === "/v1beta/openai") {
|
|
44671
44700
|
const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
|
|
44672
44701
|
url.pathname = (path10 || "/v1") + suffix;
|
|
44673
44702
|
}
|
|
@@ -44675,7 +44704,7 @@ var init_aiClient = __esm({
|
|
|
44675
44704
|
}
|
|
44676
44705
|
static headers(config) {
|
|
44677
44706
|
const headers = { "content-type": "application/json", accept: "application/json" };
|
|
44678
|
-
if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
|
|
44707
|
+
if (config.provider === "openai" || config.provider === "gemini") headers.authorization = `Bearer ${config.key}`;
|
|
44679
44708
|
if (config.provider === "anthropic") {
|
|
44680
44709
|
headers["x-api-key"] = config.key;
|
|
44681
44710
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -87,7 +87,7 @@ export interface AiChatOptions {
|
|
|
87
87
|
maxTokens?: number;
|
|
88
88
|
stream?: boolean;
|
|
89
89
|
timeout?: number;
|
|
90
|
-
provider?: "local" | "openai" | "anthropic";
|
|
90
|
+
provider?: "local" | "openai" | "anthropic" | "gemini";
|
|
91
91
|
/** Tools the model may call. ADR-0061 — translated per provider. */
|
|
92
92
|
tools?: AiToolDeclaration[];
|
|
93
93
|
/**
|
|
@@ -97,10 +97,10 @@ export interface AiChatOptions {
|
|
|
97
97
|
*/
|
|
98
98
|
toolChoice?: AiToolChoice;
|
|
99
99
|
}
|
|
100
|
-
export interface AiEmbedOptions { model?: string; timeout?: number; provider?: "local" | "openai" | "anthropic" }
|
|
100
|
+
export interface AiEmbedOptions { model?: string; timeout?: number; provider?: "local" | "openai" | "anthropic" | "gemini" }
|
|
101
101
|
|
|
102
102
|
interface Config {
|
|
103
|
-
provider: "local" | "openai" | "anthropic";
|
|
103
|
+
provider: "local" | "openai" | "anthropic" | "gemini";
|
|
104
104
|
url: string;
|
|
105
105
|
model: string;
|
|
106
106
|
key: string | null;
|
|
@@ -288,13 +288,16 @@ export class Ai {
|
|
|
288
288
|
|
|
289
289
|
private static config(capability: "chat" | "embed", options: AiChatOptions | AiEmbedOptions): Config {
|
|
290
290
|
const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
|
|
291
|
-
if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or
|
|
291
|
+
if (provider !== "local" && provider !== "openai" && provider !== "anthropic" && provider !== "gemini") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, anthropic, or gemini");
|
|
292
292
|
const key = process.env.TINA4_AI_KEY || null;
|
|
293
|
-
if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
293
|
+
if ((provider === "openai" || provider === "anthropic" || provider === "gemini") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
294
|
+
// Gemini speaks the OpenAI wire format at its OpenAI-compatible base, so it rides the openai
|
|
295
|
+
// body/parse/stream path - only the base URL, the endpoint suffix append, and the Bearer key differ.
|
|
294
296
|
const defaults: Record<Config["provider"], [string, string]> = {
|
|
295
297
|
local: ["http://localhost:11437", "llama3.2"],
|
|
296
298
|
openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
|
|
297
299
|
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"],
|
|
300
|
+
gemini: ["https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.5-flash"],
|
|
298
301
|
};
|
|
299
302
|
const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : (process.env.TINA4_AI_URL ?? defaults[provider][0]);
|
|
300
303
|
const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
|
|
@@ -309,7 +312,11 @@ export class Ai {
|
|
|
309
312
|
try { url = new URL(value); } catch { throw new AiConfigError("AI URL must be an http or https URL"); }
|
|
310
313
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
|
|
311
314
|
const path = url.pathname.replace(/\/+$/, "");
|
|
312
|
-
|
|
315
|
+
// "/v1beta/openai" is Gemini's OpenAI-compatible base; append the suffix onto it exactly as
|
|
316
|
+
// onto a bare host / "/v1" / "/api", so the default gemini base resolves to
|
|
317
|
+
// .../v1beta/openai/chat/completions (or /embeddings). A full endpoint URL the caller supplies
|
|
318
|
+
// (any other path) still passes through verbatim.
|
|
319
|
+
if (path === "" || path === "/v1" || path === "/api" || path === "/v1beta/openai") {
|
|
313
320
|
const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
|
|
314
321
|
url.pathname = (path || "/v1") + suffix;
|
|
315
322
|
}
|
|
@@ -318,7 +325,7 @@ export class Ai {
|
|
|
318
325
|
|
|
319
326
|
private static headers(config: Config): Record<string, string> {
|
|
320
327
|
const headers: Record<string, string> = { "content-type": "application/json", accept: "application/json" };
|
|
321
|
-
if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
|
|
328
|
+
if (config.provider === "openai" || config.provider === "gemini") headers.authorization = `Bearer ${config.key}`;
|
|
322
329
|
if (config.provider === "anthropic") { headers["x-api-key"] = config.key!; headers["anthropic-version"] = "2023-06-01"; }
|
|
323
330
|
return headers;
|
|
324
331
|
}
|
|
@@ -26223,13 +26223,37 @@ function swaggerEnabled() {
|
|
|
26223
26223
|
return ["true", "1", "yes", "on"].includes(raw);
|
|
26224
26224
|
}
|
|
26225
26225
|
function createSwaggerRoutes(getSpec) {
|
|
26226
|
+
const serveUi = async (_req, res) => {
|
|
26227
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
26228
|
+
};
|
|
26226
26229
|
return [
|
|
26227
26230
|
{
|
|
26228
26231
|
method: "GET",
|
|
26229
26232
|
pattern: "/swagger",
|
|
26230
|
-
handler:
|
|
26231
|
-
|
|
26232
|
-
|
|
26233
|
+
handler: serveUi
|
|
26234
|
+
},
|
|
26235
|
+
{
|
|
26236
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
26237
|
+
//
|
|
26238
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
26239
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
26240
|
+
// this route and was answered by the framework-bundled
|
|
26241
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
26242
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
26243
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
26244
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
26245
|
+
// cdnjs, while the page this handler renders loads from
|
|
26246
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
26247
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
26248
|
+
//
|
|
26249
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
26250
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
26251
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
26252
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
26253
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
26254
|
+
method: "GET",
|
|
26255
|
+
pattern: "/swagger/",
|
|
26256
|
+
handler: serveUi
|
|
26233
26257
|
},
|
|
26234
26258
|
{
|
|
26235
26259
|
method: "GET",
|
|
@@ -33463,13 +33487,14 @@ var init_aiClient = __esm({
|
|
|
33463
33487
|
}
|
|
33464
33488
|
static config(capability, options) {
|
|
33465
33489
|
const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
|
|
33466
|
-
if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or
|
|
33490
|
+
if (provider !== "local" && provider !== "openai" && provider !== "anthropic" && provider !== "gemini") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, anthropic, or gemini");
|
|
33467
33491
|
const key = process.env.TINA4_AI_KEY || null;
|
|
33468
|
-
if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
33492
|
+
if ((provider === "openai" || provider === "anthropic" || provider === "gemini") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
33469
33493
|
const defaults = {
|
|
33470
33494
|
local: ["http://localhost:11437", "llama3.2"],
|
|
33471
33495
|
openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
|
|
33472
|
-
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
|
|
33496
|
+
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"],
|
|
33497
|
+
gemini: ["https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.5-flash"]
|
|
33473
33498
|
};
|
|
33474
33499
|
const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
|
|
33475
33500
|
const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
|
|
@@ -33487,7 +33512,7 @@ var init_aiClient = __esm({
|
|
|
33487
33512
|
}
|
|
33488
33513
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
|
|
33489
33514
|
const path10 = url.pathname.replace(/\/+$/, "");
|
|
33490
|
-
if (path10 === "" || path10 === "/v1" || path10 === "/api") {
|
|
33515
|
+
if (path10 === "" || path10 === "/v1" || path10 === "/api" || path10 === "/v1beta/openai") {
|
|
33491
33516
|
const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
|
|
33492
33517
|
url.pathname = (path10 || "/v1") + suffix;
|
|
33493
33518
|
}
|
|
@@ -33495,7 +33520,7 @@ var init_aiClient = __esm({
|
|
|
33495
33520
|
}
|
|
33496
33521
|
static headers(config) {
|
|
33497
33522
|
const headers = { "content-type": "application/json", accept: "application/json" };
|
|
33498
|
-
if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
|
|
33523
|
+
if (config.provider === "openai" || config.provider === "gemini") headers.authorization = `Bearer ${config.key}`;
|
|
33499
33524
|
if (config.provider === "anthropic") {
|
|
33500
33525
|
headers["x-api-key"] = config.key;
|
|
33501
33526
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -41775,6 +41800,10 @@ async function recordApplied(db, name, batch, passed = 1) {
|
|
|
41775
41800
|
insertCols.push("migration_id");
|
|
41776
41801
|
values.push(name);
|
|
41777
41802
|
}
|
|
41803
|
+
if (cols.has("name")) {
|
|
41804
|
+
insertCols.push("name");
|
|
41805
|
+
values.push(name);
|
|
41806
|
+
}
|
|
41778
41807
|
if (isFirebirdAdapter(db)) {
|
|
41779
41808
|
const rows = await adapterQuery(
|
|
41780
41809
|
db,
|
|
@@ -524,6 +524,19 @@ async function recordApplied(
|
|
|
524
524
|
values.push(name);
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
// A table created by an OLDER Node v3 (<= 3.13.54) carries `name VARCHAR(500)
|
|
528
|
+
// NOT NULL` (the pre-3.13.55 tracking DDL — see git history). upgradeMigrationTable()
|
|
529
|
+
// copies its values into migration_name and leaves the `name` column in place
|
|
530
|
+
// (SQLite cannot drop a NOT NULL column without a full rebuild). Exactly like the
|
|
531
|
+
// python#93 `migration_id` case above, an insert that omits `name` fails its
|
|
532
|
+
// NOT NULL constraint, wedging EVERY migration recorded after the upgrade — so
|
|
533
|
+
// populate it here too, mirroring migration_name. A fresh canonical table has no
|
|
534
|
+
// `name` column, so this only fires on an upgraded legacy table.
|
|
535
|
+
if (cols.has("name")) {
|
|
536
|
+
insertCols.push("name");
|
|
537
|
+
values.push(name);
|
|
538
|
+
}
|
|
539
|
+
|
|
527
540
|
if (isFirebirdAdapter(db)) {
|
|
528
541
|
// Firebird: generate the id from the sequence.
|
|
529
542
|
const rows = await adapterQuery<{ next_id: number }>(db,
|
|
@@ -521,13 +521,37 @@ function swaggerEnabled() {
|
|
|
521
521
|
return ["true", "1", "yes", "on"].includes(raw);
|
|
522
522
|
}
|
|
523
523
|
function createSwaggerRoutes(getSpec) {
|
|
524
|
+
const serveUi = async (_req, res) => {
|
|
525
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
526
|
+
};
|
|
524
527
|
return [
|
|
525
528
|
{
|
|
526
529
|
method: "GET",
|
|
527
530
|
pattern: "/swagger",
|
|
528
|
-
handler:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
+
handler: serveUi
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
535
|
+
//
|
|
536
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
537
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
538
|
+
// this route and was answered by the framework-bundled
|
|
539
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
540
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
541
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
542
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
543
|
+
// cdnjs, while the page this handler renders loads from
|
|
544
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
545
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
546
|
+
//
|
|
547
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
548
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
549
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
550
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
551
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
552
|
+
method: "GET",
|
|
553
|
+
pattern: "/swagger/",
|
|
554
|
+
handler: serveUi
|
|
531
555
|
},
|
|
532
556
|
{
|
|
533
557
|
method: "GET",
|
|
@@ -56,13 +56,38 @@ export function swaggerEnabled(): boolean {
|
|
|
56
56
|
export function createSwaggerRoutes(
|
|
57
57
|
getSpec: () => unknown
|
|
58
58
|
): RouteDefinition[] {
|
|
59
|
+
const serveUi = async (_req: Tina4Request, res: Tina4Response): Promise<void> => {
|
|
60
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
61
|
+
};
|
|
62
|
+
|
|
59
63
|
return [
|
|
60
64
|
{
|
|
61
65
|
method: "GET",
|
|
62
66
|
pattern: "/swagger",
|
|
63
|
-
handler:
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
handler: serveUi,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
71
|
+
//
|
|
72
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
73
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
74
|
+
// this route and was answered by the framework-bundled
|
|
75
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
76
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
77
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
78
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
79
|
+
// cdnjs, while the page this handler renders loads from
|
|
80
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
81
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
82
|
+
//
|
|
83
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
84
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
85
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
86
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
87
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
88
|
+
method: "GET",
|
|
89
|
+
pattern: "/swagger/",
|
|
90
|
+
handler: serveUi,
|
|
66
91
|
},
|
|
67
92
|
{
|
|
68
93
|
method: "GET",
|
|
@@ -113,7 +113,7 @@ export interface AiChatOptions {
|
|
|
113
113
|
maxTokens?: number;
|
|
114
114
|
stream?: boolean;
|
|
115
115
|
timeout?: number;
|
|
116
|
-
provider?: "local" | "openai" | "anthropic";
|
|
116
|
+
provider?: "local" | "openai" | "anthropic" | "gemini";
|
|
117
117
|
/** Tools the model may call. ADR-0061 — translated per provider. */
|
|
118
118
|
tools?: AiToolDeclaration[];
|
|
119
119
|
/**
|
|
@@ -126,7 +126,7 @@ export interface AiChatOptions {
|
|
|
126
126
|
export interface AiEmbedOptions {
|
|
127
127
|
model?: string;
|
|
128
128
|
timeout?: number;
|
|
129
|
-
provider?: "local" | "openai" | "anthropic";
|
|
129
|
+
provider?: "local" | "openai" | "anthropic" | "gemini";
|
|
130
130
|
}
|
|
131
131
|
export declare class Ai {
|
|
132
132
|
static chat(messages: AiMessage[], options: AiChatOptions & {
|