@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 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.0",
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.1000",
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.0",
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",
@@ -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 Kimi Code
273
- kilo Kilo Gateway
274
- zai Z.AI (GLM Coding Plan)
275
- nanogpt NanoGPT
276
- minimax-code MiniMax Coding Plan (International)
277
- minimax-code-cn MiniMax Coding Plan (China)
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:
@@ -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
+ }
@@ -20,6 +20,7 @@ export type OAuthProvider =
20
20
  | "huggingface"
21
21
  | "kimi-code"
22
22
  | "kilo"
23
+ | "kagi"
23
24
  | "litellm"
24
25
  | "lm-studio"
25
26
  | "minimax-code"
@@ -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,5 +1,6 @@
1
1
  export * from "./adapt";
2
2
  export * from "./compatibility";
3
+ export * from "./dereference";
3
4
  export * from "./equality";
4
5
  export * from "./fields";
5
6
  export * from "./normalize-cca";
@@ -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
- return sanitizeSchemaImpl(value, {
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,
@@ -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
- // Compile the schema
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)) {