@oh-my-pi/pi-ai 13.7.0 → 13.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/package.json +3 -3
- package/src/auth-storage.ts +6 -0
- package/src/cli.ts +24 -6
- package/src/utils/oauth/index.ts +9 -0
- package/src/utils/oauth/kagi.ts +46 -0
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/schema/dereference.ts +93 -0
- package/src/utils/schema/index.ts +1 -0
- package/src/utils/schema/sanitize-google.ts +6 -1
- package/src/utils/validation.ts +17 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.7.2] - 2026-03-04
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added support for Kagi API key authentication via `login kagi` command
|
|
9
|
+
- Added Kagi to the list of available OAuth providers
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- MCP tool schemas with `$ref`/`$defs` are now dereferenced before being sent to LLM providers, fixing dangling references that left models without type definitions
|
|
14
|
+
- Ajv schema validation no longer emits `console.warn()` for non-standard format keywords (e.g. `"uint"`) from MCP servers, preventing TUI corruption
|
|
15
|
+
- Tool schema compilation is now cached per schema identity, eliminating redundant recompilation on every tool call
|
|
16
|
+
|
|
5
17
|
## [13.6.0] - 2026-03-03
|
|
6
18
|
### Added
|
|
7
19
|
|
|
@@ -1515,4 +1527,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
|
|
|
1515
1527
|
|
|
1516
1528
|
## [0.9.4] - 2025-11-26
|
|
1517
1529
|
|
|
1518
|
-
Initial release with multi-provider LLM support.
|
|
1530
|
+
Initial release with multi-provider LLM support.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "13.7.
|
|
4
|
+
"version": "13.7.3",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@anthropic-ai/sdk": "^0.78",
|
|
41
|
-
"@aws-sdk/client-bedrock-runtime": "^3
|
|
41
|
+
"@aws-sdk/client-bedrock-runtime": "^3",
|
|
42
42
|
"@bufbuild/protobuf": "^2.11",
|
|
43
43
|
"@google/genai": "^1.43",
|
|
44
|
-
"@oh-my-pi/pi-utils": "13.7.
|
|
44
|
+
"@oh-my-pi/pi-utils": "13.7.3",
|
|
45
45
|
"@sinclair/typebox": "^0.34",
|
|
46
46
|
"@smithy/node-http-handler": "^4.4",
|
|
47
47
|
"ajv": "^8.18",
|
package/src/auth-storage.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { loginGitLabDuo } from "./utils/oauth/gitlab-duo";
|
|
|
42
42
|
import { loginAntigravity } from "./utils/oauth/google-antigravity";
|
|
43
43
|
import { loginGeminiCli } from "./utils/oauth/google-gemini-cli";
|
|
44
44
|
import { loginHuggingface } from "./utils/oauth/huggingface";
|
|
45
|
+
import { loginKagi } from "./utils/oauth/kagi";
|
|
45
46
|
import { loginKilo } from "./utils/oauth/kilo";
|
|
46
47
|
import { loginKimi } from "./utils/oauth/kimi";
|
|
47
48
|
import { loginLiteLLM } from "./utils/oauth/litellm";
|
|
@@ -879,6 +880,11 @@ export class AuthStorage {
|
|
|
879
880
|
await saveApiKeyCredential(apiKey);
|
|
880
881
|
return;
|
|
881
882
|
}
|
|
883
|
+
case "kagi": {
|
|
884
|
+
const apiKey = await loginKagi(ctrl);
|
|
885
|
+
await saveApiKeyCredential(apiKey);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
882
888
|
case "nanogpt": {
|
|
883
889
|
const apiKey = await loginNanoGPT(ctrl);
|
|
884
890
|
await saveApiKeyCredential(apiKey);
|
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { loginCursor } from "./utils/oauth/cursor";
|
|
|
7
7
|
import { loginGitHubCopilot } from "./utils/oauth/github-copilot";
|
|
8
8
|
import { loginAntigravity } from "./utils/oauth/google-antigravity";
|
|
9
9
|
import { loginGeminiCli } from "./utils/oauth/google-gemini-cli";
|
|
10
|
+
import { loginKagi } from "./utils/oauth/kagi";
|
|
10
11
|
import { loginKilo } from "./utils/oauth/kilo";
|
|
11
12
|
import { loginKimi } from "./utils/oauth/kimi";
|
|
12
13
|
import { loginMiniMaxCode, loginMiniMaxCodeCn } from "./utils/oauth/minimax-code";
|
|
@@ -157,6 +158,22 @@ async function login(provider: OAuthProvider): Promise<void> {
|
|
|
157
158
|
},
|
|
158
159
|
});
|
|
159
160
|
break;
|
|
161
|
+
case "kagi": {
|
|
162
|
+
const apiKey = await loginKagi({
|
|
163
|
+
onAuth(info) {
|
|
164
|
+
const { url, instructions } = info;
|
|
165
|
+
console.log(`\nOpen this URL in your browser:\n${url}`);
|
|
166
|
+
if (instructions) console.log(instructions);
|
|
167
|
+
console.log();
|
|
168
|
+
},
|
|
169
|
+
onPrompt(p) {
|
|
170
|
+
return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
storage.saveApiKey(provider, apiKey);
|
|
174
|
+
console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
160
177
|
|
|
161
178
|
case "cursor":
|
|
162
179
|
credentials = await loginCursor(
|
|
@@ -269,12 +286,13 @@ Providers:
|
|
|
269
286
|
google-gemini-cli Google Gemini CLI
|
|
270
287
|
google-antigravity Antigravity (Gemini 3, Claude, GPT-OSS)
|
|
271
288
|
openai-codex OpenAI Codex (ChatGPT Plus/Pro)
|
|
272
|
-
kimi-code
|
|
273
|
-
kilo
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
289
|
+
kimi-code Kimi Code
|
|
290
|
+
kilo Kilo Gateway
|
|
291
|
+
kagi Kagi
|
|
292
|
+
zai Z.AI (GLM Coding Plan)
|
|
293
|
+
nanogpt NanoGPT
|
|
294
|
+
minimax-code MiniMax Coding Plan (International)
|
|
295
|
+
minimax-code-cn MiniMax Coding Plan (China)
|
|
278
296
|
cursor Cursor (Claude, GPT, etc.)
|
|
279
297
|
|
|
280
298
|
Examples:
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
* - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud)
|
|
29
29
|
* - Kimi Code
|
|
30
30
|
* - Kilo Gateway
|
|
31
|
+
* - Kagi
|
|
31
32
|
* - Cerebras
|
|
32
33
|
* - Hugging Face Inference
|
|
33
34
|
* - Synthetic
|
|
@@ -66,6 +67,8 @@ export { loginAntigravity, refreshAntigravityToken } from "./google-antigravity"
|
|
|
66
67
|
export { loginGeminiCli, refreshGoogleCloudToken } from "./google-gemini-cli";
|
|
67
68
|
// Hugging Face Inference (API key)
|
|
68
69
|
export { loginHuggingface } from "./huggingface";
|
|
70
|
+
// Kagi (API key)
|
|
71
|
+
export { loginKagi } from "./kagi";
|
|
69
72
|
// Kilo Gateway
|
|
70
73
|
export { loginKilo } from "./kilo";
|
|
71
74
|
// Kimi Code
|
|
@@ -135,6 +138,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
135
138
|
name: "Kilo Gateway",
|
|
136
139
|
available: true,
|
|
137
140
|
},
|
|
141
|
+
{
|
|
142
|
+
id: "kagi",
|
|
143
|
+
name: "Kagi",
|
|
144
|
+
available: true,
|
|
145
|
+
},
|
|
138
146
|
{
|
|
139
147
|
id: "cerebras",
|
|
140
148
|
name: "Cerebras",
|
|
@@ -354,6 +362,7 @@ export async function refreshOAuthToken(
|
|
|
354
362
|
case "minimax-code":
|
|
355
363
|
case "minimax-code-cn":
|
|
356
364
|
case "moonshot":
|
|
365
|
+
case "kagi":
|
|
357
366
|
case "cloudflare-ai-gateway":
|
|
358
367
|
case "qwen-portal":
|
|
359
368
|
case "vllm":
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kagi login flow.
|
|
3
|
+
*
|
|
4
|
+
* Kagi web search uses an API key from the account settings page.
|
|
5
|
+
* This is an API key flow:
|
|
6
|
+
* 1. Open browser to Kagi API settings
|
|
7
|
+
* 2. User copies API key
|
|
8
|
+
* 3. User pastes key into CLI
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { OAuthController } from "./types";
|
|
12
|
+
|
|
13
|
+
const AUTH_URL = "https://kagi.com/settings/api";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Login to Kagi.
|
|
17
|
+
*
|
|
18
|
+
* Opens browser to API settings and prompts user to paste their API key.
|
|
19
|
+
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
20
|
+
*/
|
|
21
|
+
export async function loginKagi(options: OAuthController): Promise<string> {
|
|
22
|
+
if (!options.onPrompt) {
|
|
23
|
+
throw new Error("Kagi login requires onPrompt callback");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
options.onAuth?.({
|
|
27
|
+
url: AUTH_URL,
|
|
28
|
+
instructions: "Copy your API key from Kagi API settings",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const apiKey = await options.onPrompt({
|
|
32
|
+
message: "Paste your Kagi API key",
|
|
33
|
+
placeholder: "kagi_...",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (options.signal?.aborted) {
|
|
37
|
+
throw new Error("Login cancelled");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const trimmed = apiKey.trim();
|
|
41
|
+
if (!trimmed) {
|
|
42
|
+
throw new Error("API key is required");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return trimmed;
|
|
46
|
+
}
|
package/src/utils/oauth/types.ts
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline `$ref` / `$defs` in a JSON Schema so every consumer sees
|
|
3
|
+
* the full definition without needing a resolver.
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Local `$ref` pointers (`#/$defs/Foo`, `#/definitions/Foo`)
|
|
7
|
+
* - Nested `$defs` / `definitions` blocks
|
|
8
|
+
* - Circular references (breaks the cycle by emitting `{}`)
|
|
9
|
+
*
|
|
10
|
+
* After dereferencing, `$defs` and `definitions` are stripped from the root.
|
|
11
|
+
*/
|
|
12
|
+
import { isJsonObject, type JsonObject } from "./types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a JSON-pointer-style `$ref` against the root schema's `$defs`
|
|
16
|
+
* or `definitions` block. Returns `undefined` for external or unresolvable refs.
|
|
17
|
+
*/
|
|
18
|
+
function resolveLocalRef(ref: string, root: JsonObject): JsonObject | undefined {
|
|
19
|
+
// Only handle local refs: #/$defs/Name or #/definitions/Name
|
|
20
|
+
const match = /^#\/(\$defs|definitions)\/(.+)$/.exec(ref);
|
|
21
|
+
if (!match) return undefined;
|
|
22
|
+
|
|
23
|
+
const [, defsKey, name] = match;
|
|
24
|
+
const defs = root[defsKey!];
|
|
25
|
+
if (!isJsonObject(defs)) return undefined;
|
|
26
|
+
|
|
27
|
+
const resolved = defs[name!];
|
|
28
|
+
return isJsonObject(resolved) ? resolved : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Recursively dereference a JSON Schema node, inlining all local `$ref` pointers.
|
|
33
|
+
*/
|
|
34
|
+
function dereferenceNode(node: unknown, root: JsonObject, visiting: Set<string>): unknown {
|
|
35
|
+
if (!isJsonObject(node)) return node;
|
|
36
|
+
if (Array.isArray(node)) return node.map(item => dereferenceNode(item, root, visiting));
|
|
37
|
+
|
|
38
|
+
const ref = node.$ref;
|
|
39
|
+
if (typeof ref === "string") {
|
|
40
|
+
// Break circular references
|
|
41
|
+
if (visiting.has(ref)) return {};
|
|
42
|
+
const resolved = resolveLocalRef(ref, root);
|
|
43
|
+
if (!resolved) return node; // External ref — leave as-is
|
|
44
|
+
visiting.add(ref);
|
|
45
|
+
const inlined = dereferenceNode(resolved, root, visiting);
|
|
46
|
+
visiting.delete(ref);
|
|
47
|
+
|
|
48
|
+
// Merge sibling keywords (e.g. description, default) from the
|
|
49
|
+
// referencing node. In draft 2020-12 these are valid alongside $ref.
|
|
50
|
+
const hasSiblings = Object.keys(node).some(k => k !== "$ref");
|
|
51
|
+
if (!hasSiblings || !isJsonObject(inlined)) return inlined;
|
|
52
|
+
const merged: JsonObject = { ...inlined };
|
|
53
|
+
for (const [key, value] of Object.entries(node)) {
|
|
54
|
+
if (key !== "$ref") merged[key] = value;
|
|
55
|
+
}
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result: JsonObject = {};
|
|
60
|
+
for (const [key, value] of Object.entries(node)) {
|
|
61
|
+
// Skip $defs/definitions — they get inlined into consumers
|
|
62
|
+
if (key === "$defs" || key === "definitions") continue;
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
result[key] = value.map(item => dereferenceNode(item, root, visiting));
|
|
66
|
+
} else if (isJsonObject(value)) {
|
|
67
|
+
result[key] = dereferenceNode(value, root, visiting);
|
|
68
|
+
} else {
|
|
69
|
+
result[key] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Dereference all local `$ref` pointers in a JSON Schema, inlining definitions
|
|
77
|
+
* from `$defs` / `definitions`. The `$defs` block is stripped from the output.
|
|
78
|
+
*
|
|
79
|
+
* Non-local refs (e.g. `http://...`) are left untouched.
|
|
80
|
+
* Circular references are broken with `{}`.
|
|
81
|
+
*
|
|
82
|
+
* @returns A new schema object with all local refs inlined, or the input unchanged
|
|
83
|
+
* if it's not an object or has no `$defs`/`definitions`.
|
|
84
|
+
*/
|
|
85
|
+
export function dereferenceJsonSchema(schema: unknown): unknown {
|
|
86
|
+
if (!isJsonObject(schema)) return schema;
|
|
87
|
+
|
|
88
|
+
// Fast path: nothing to dereference
|
|
89
|
+
const hasDefs = schema.$defs !== undefined || schema.definitions !== undefined;
|
|
90
|
+
if (!hasDefs) return schema;
|
|
91
|
+
|
|
92
|
+
return dereferenceNode(schema, schema, new Set());
|
|
93
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dereferenceJsonSchema } from "./dereference";
|
|
1
2
|
import { areJsonValuesEqual } from "./equality";
|
|
2
3
|
import { UNSUPPORTED_SCHEMA_FIELDS } from "./fields";
|
|
3
4
|
|
|
@@ -197,7 +198,11 @@ const MCP_UNSUPPORTED_SCHEMA_FIELDS = new Set(["$schema"]);
|
|
|
197
198
|
* (`pattern`, `format`, `additionalProperties`, etc.) and `$ref`/`$defs`.
|
|
198
199
|
*/
|
|
199
200
|
export function sanitizeSchemaForMCP(value: unknown): unknown {
|
|
200
|
-
|
|
201
|
+
// Dereference $ref/$defs first — MCP servers emit standard JSON Schema
|
|
202
|
+
// with $defs, but providers (Anthropic, Google) only forward `properties`
|
|
203
|
+
// and `required`, dropping $defs and leaving dangling $ref pointers.
|
|
204
|
+
const dereferenced = dereferenceJsonSchema(value);
|
|
205
|
+
return sanitizeSchemaImpl(dereferenced, {
|
|
201
206
|
insideProperties: false,
|
|
202
207
|
normalizeTypeArrayToNullable: false,
|
|
203
208
|
stripNullableKeyword: true,
|
package/src/utils/validation.ts
CHANGED
|
@@ -451,12 +451,28 @@ function coerceArgsFromErrors(
|
|
|
451
451
|
|
|
452
452
|
// Create a singleton AJV instance with formats (only if not in browser extension)
|
|
453
453
|
// AJV requires 'unsafe-eval' CSP which is not allowed in Manifest V3
|
|
454
|
+
//
|
|
455
|
+
// Silent logger: MCP servers may declare non-standard format keywords (e.g. "uint")
|
|
456
|
+
// which cause Ajv to emit console.warn() with strict:false — corrupting TUI output.
|
|
454
457
|
const ajv = new Ajv({
|
|
455
458
|
allErrors: true,
|
|
456
459
|
strict: false,
|
|
460
|
+
logger: false,
|
|
457
461
|
});
|
|
458
462
|
addFormats(ajv);
|
|
459
463
|
|
|
464
|
+
// Cache compiled validators by schema object identity to avoid
|
|
465
|
+
// re-compiling the same tool schema on every call.
|
|
466
|
+
const compiledSchemaCache = new WeakMap<object, import("ajv").ValidateFunction>();
|
|
467
|
+
function compileSchema(schema: object): import("ajv").ValidateFunction {
|
|
468
|
+
let validate = compiledSchemaCache.get(schema);
|
|
469
|
+
if (!validate) {
|
|
470
|
+
validate = ajv.compile(schema);
|
|
471
|
+
compiledSchemaCache.set(schema, validate);
|
|
472
|
+
}
|
|
473
|
+
return validate;
|
|
474
|
+
}
|
|
475
|
+
|
|
460
476
|
const MAX_TYPE_COERCION_PASSES = 5;
|
|
461
477
|
|
|
462
478
|
/**
|
|
@@ -484,8 +500,7 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): any {
|
|
|
484
500
|
export function validateToolArguments(tool: Tool, toolCall: ToolCall): any {
|
|
485
501
|
const originalArgs = toolCall.arguments;
|
|
486
502
|
|
|
487
|
-
|
|
488
|
-
const validate = ajv.compile(tool.parameters);
|
|
503
|
+
const validate = compileSchema(tool.parameters);
|
|
489
504
|
|
|
490
505
|
// Validate the arguments
|
|
491
506
|
if (validate(originalArgs)) {
|