@oh-my-pi/pi-coding-agent 13.9.3 → 13.9.5

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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.9.4] - 2026-03-07
6
+ ### Added
7
+
8
+ - Automatic detection of Ollama model capabilities including reasoning/thinking support and vision input via the `/api/show` endpoint
9
+ - Improved Kagi API error handling with extraction of detailed error messages from JSON and plain text responses
10
+
11
+ ### Changed
12
+
13
+ - Updated Kagi provider description to clarify requirement for Kagi Search API beta access
14
+
5
15
  ## [13.9.3] - 2026-03-07
6
16
 
7
17
  ### Breaking Changes
@@ -54,6 +64,7 @@
54
64
  - Fixed model registry to preserve explicit thinking configuration on runtime-registered models
55
65
  - Fixed usage limit reset time calculation to use absolute `resetsAt` timestamps instead of deprecated `resetInMs` field
56
66
  - Fixed compaction summary message creation to no longer be automatically added to chat during compaction (now handled by session manager)
67
+ - Fixed Kagi web search errors to surface the provider's beta-access message and clarified that Kagi search requires Search API beta access
57
68
 
58
69
  ## [13.9.2] - 2026-03-05
59
70
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.9.3",
4
+ "version": "13.9.5",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.9.3",
45
- "@oh-my-pi/pi-agent-core": "13.9.3",
46
- "@oh-my-pi/pi-ai": "13.9.3",
47
- "@oh-my-pi/pi-natives": "13.9.3",
48
- "@oh-my-pi/pi-tui": "13.9.3",
49
- "@oh-my-pi/pi-utils": "13.9.3",
44
+ "@oh-my-pi/omp-stats": "13.9.5",
45
+ "@oh-my-pi/pi-agent-core": "13.9.5",
46
+ "@oh-my-pi/pi-ai": "13.9.5",
47
+ "@oh-my-pi/pi-natives": "13.9.5",
48
+ "@oh-my-pi/pi-tui": "13.9.5",
49
+ "@oh-my-pi/pi-utils": "13.9.5",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
package/src/cli/args.ts CHANGED
@@ -108,7 +108,10 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
108
108
  } else if (arg === "--no-pty") {
109
109
  result.noPty = true;
110
110
  } else if (arg === "--tools" && i + 1 < args.length) {
111
- const toolNames = args[++i].split(",").map(s => s.trim());
111
+ const toolNames = args[++i]
112
+ .split(",")
113
+ .map(s => s.trim().toLowerCase())
114
+ .filter(Boolean);
112
115
  const validTools: string[] = [];
113
116
  for (const name of toolNames) {
114
117
  if (name in BUILTIN_TOOLS) {
@@ -23,7 +23,7 @@ import {
23
23
  unregisterCustomApis,
24
24
  unregisterOAuthProviders,
25
25
  } from "@oh-my-pi/pi-ai";
26
- import { logger } from "@oh-my-pi/pi-utils";
26
+ import { isRecord, logger } from "@oh-my-pi/pi-utils";
27
27
  import { type Static, Type } from "@sinclair/typebox";
28
28
  import { type ConfigError, ConfigFile } from "../config";
29
29
  import type { ThemeColor } from "../modes/theme/theme";
@@ -862,12 +862,57 @@ export class ModelRegistry {
862
862
  }
863
863
  }
864
864
 
865
+ async #discoverOllamaModelMetadata(
866
+ endpoint: string,
867
+ modelId: string,
868
+ headers: Record<string, string> | undefined,
869
+ ): Promise<{ reasoning: boolean; input: ("text" | "image")[] } | null> {
870
+ const showUrl = `${endpoint}/api/show`;
871
+ try {
872
+ const response = await fetch(showUrl, {
873
+ method: "POST",
874
+ headers: { ...(headers ?? {}), "Content-Type": "application/json" },
875
+ body: JSON.stringify({ model: modelId }),
876
+ signal: AbortSignal.timeout(1500),
877
+ });
878
+ if (!response.ok) {
879
+ return null;
880
+ }
881
+ const payload = (await response.json()) as unknown;
882
+ if (!isRecord(payload)) {
883
+ return null;
884
+ }
885
+ const capabilities = payload.capabilities;
886
+ if (Array.isArray(capabilities)) {
887
+ const normalized = new Set(
888
+ capabilities.flatMap(capability => (typeof capability === "string" ? [capability.toLowerCase()] : [])),
889
+ );
890
+ const supportsVision = normalized.has("vision") || normalized.has("image");
891
+ return {
892
+ reasoning: normalized.has("thinking"),
893
+ input: supportsVision ? ["text", "image"] : ["text"],
894
+ };
895
+ }
896
+ if (!isRecord(capabilities)) {
897
+ return null;
898
+ }
899
+ const supportsVision = capabilities.vision === true || capabilities.image === true;
900
+ return {
901
+ reasoning: capabilities.thinking === true,
902
+ input: supportsVision ? ["text", "image"] : ["text"],
903
+ };
904
+ } catch {
905
+ return null;
906
+ }
907
+ }
908
+
865
909
  async #discoverOllamaModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
866
910
  const endpoint = this.#normalizeOllamaBaseUrl(providerConfig.baseUrl);
867
911
  const tagsUrl = `${endpoint}/api/tags`;
912
+ const headers = { ...(providerConfig.headers ?? {}) };
868
913
  try {
869
914
  const response = await fetch(tagsUrl, {
870
- headers: { ...(providerConfig.headers ?? {}) },
915
+ headers,
871
916
  signal: AbortSignal.timeout(3000),
872
917
  });
873
918
  if (!response.ok) {
@@ -879,27 +924,34 @@ export class ModelRegistry {
879
924
  return [];
880
925
  }
881
926
  const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
882
- const models = payload.models ?? [];
883
- const discovered: Model<Api>[] = [];
884
- for (const item of models) {
927
+ const entries = (payload.models ?? []).flatMap(item => {
885
928
  const id = item.model || item.name;
886
- if (!id) continue;
887
- discovered.push(
888
- enrichModelThinking({
889
- id,
890
- name: item.name || id,
891
- api: providerConfig.api,
892
- provider: providerConfig.provider,
893
- baseUrl: `${endpoint}/v1`,
894
- reasoning: false,
895
- input: ["text"],
896
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
897
- contextWindow: 128000,
898
- maxTokens: 8192,
899
- headers: providerConfig.headers,
900
- }),
901
- );
902
- }
929
+ return id ? [{ id, name: item.name || id }] : [];
930
+ });
931
+ const metadataById = new Map(
932
+ await Promise.all(
933
+ entries.map(
934
+ async entry =>
935
+ [entry.id, await this.#discoverOllamaModelMetadata(endpoint, entry.id, headers)] as const,
936
+ ),
937
+ ),
938
+ );
939
+ const discovered = entries.map(entry => {
940
+ const metadata = metadataById.get(entry.id);
941
+ return enrichModelThinking({
942
+ id: entry.id,
943
+ name: entry.name,
944
+ api: providerConfig.api,
945
+ provider: providerConfig.provider,
946
+ baseUrl: `${endpoint}/v1`,
947
+ reasoning: metadata?.reasoning ?? false,
948
+ input: metadata?.input ?? ["text"],
949
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
950
+ contextWindow: 128000,
951
+ maxTokens: 8192,
952
+ headers: providerConfig.headers,
953
+ });
954
+ });
903
955
  return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
904
956
  } catch (error) {
905
957
  logger.warn("model discovery failed for provider", {
@@ -206,7 +206,7 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
206
206
  return null;
207
207
  }
208
208
 
209
- let tools = parseArrayOrCSV(frontmatter.tools);
209
+ let tools = parseArrayOrCSV(frontmatter.tools)?.map(tool => tool.toLowerCase());
210
210
 
211
211
  // Subagents with explicit tool lists always need submit_result
212
212
  if (tools && !tools.includes("submit_result")) {
@@ -234,7 +234,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
234
234
  { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY" },
235
235
  { value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
236
236
  { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
237
- { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY" },
237
+ { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
238
238
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
239
239
  ],
240
240
  "providers.image": [
@@ -7,6 +7,7 @@ import { settings } from "../../config/settings";
7
7
  import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../config/settings-schema";
8
8
  import { theme } from "../../modes/theme/theme";
9
9
  import type { AgentSession } from "../../session/agent-session";
10
+ import { calculatePromptTokens } from "../../session/compaction/compaction";
10
11
  import { findGitHeadPathSync, sanitizeStatusText } from "../shared";
11
12
  import {
12
13
  canReuseCachedPr,
@@ -365,12 +366,7 @@ export class StatusLineComponent implements Component {
365
366
  .reverse()
366
367
  .find(m => m.role === "assistant" && m.stopReason !== "aborted") as AssistantMessage | undefined;
367
368
 
368
- const contextTokens = lastAssistantMessage
369
- ? lastAssistantMessage.usage.input +
370
- lastAssistantMessage.usage.output +
371
- lastAssistantMessage.usage.cacheRead +
372
- lastAssistantMessage.usage.cacheWrite
373
- : 0;
369
+ const contextTokens = lastAssistantMessage ? calculatePromptTokens(lastAssistantMessage.usage) : 0;
374
370
  const contextWindow = state.model?.contextWindow || 0;
375
371
  const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
376
372
 
package/src/sdk.ts CHANGED
@@ -1251,9 +1251,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1251
1251
  };
1252
1252
 
1253
1253
  const toolNamesFromRegistry = Array.from(toolRegistry.keys());
1254
- const requestedToolNames = options.toolNames ?? toolNamesFromRegistry;
1254
+ const requestedToolNames = options.toolNames?.map(name => name.toLowerCase()) ?? toolNamesFromRegistry;
1255
1255
  const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
1256
- const includeExitPlanMode = options.toolNames?.includes("exit_plan_mode") ?? false;
1256
+ const includeExitPlanMode = requestedToolNames.includes("exit_plan_mode");
1257
1257
  const initialToolNames = includeExitPlanMode
1258
1258
  ? normalizedRequested
1259
1259
  : normalizedRequested.filter(name => name !== "exit_plan_mode");
@@ -108,6 +108,7 @@ import { extractFileMentions, generateFileMentionMessages } from "../utils/file-
108
108
  import {
109
109
  type CompactionResult,
110
110
  calculateContextTokens,
111
+ calculatePromptTokens,
111
112
  collectEntriesForBranchSummary,
112
113
  compact,
113
114
  estimateTokens,
@@ -5076,7 +5077,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
5076
5077
  };
5077
5078
  }
5078
5079
 
5079
- const usageTokens = calculateContextTokens(lastUsage);
5080
+ const usageTokens = calculatePromptTokens(lastUsage);
5080
5081
  let trailingTokens = 0;
5081
5082
  for (let i = lastUsageIndex + 1; i < messages.length; i++) {
5082
5083
  trailingTokens += estimateTokens(messages[i]);
@@ -227,7 +227,8 @@ function getPythonModeFromEnv(): PythonToolMode | null {
227
227
  export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
228
228
  const includeSubmitResult = session.requireSubmitResultTool === true;
229
229
  const enableLsp = session.enableLsp ?? true;
230
- const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
230
+ const requestedTools =
231
+ toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
231
232
  if (requestedTools && !requestedTools.includes("exit_plan_mode")) {
232
233
  requestedTools.push("exit_plan_mode");
233
234
  }
package/src/web/kagi.ts CHANGED
@@ -28,15 +28,23 @@ interface KagiRelatedSearchesObject {
28
28
 
29
29
  type KagiSearchObject = KagiSearchResultObject | KagiRelatedSearchesObject;
30
30
 
31
+ interface KagiErrorEntry {
32
+ code?: number;
33
+ msg?: string;
34
+ }
35
+
31
36
  interface KagiSearchResponse {
32
37
  meta: {
33
38
  id: string;
34
39
  };
35
40
  data: KagiSearchObject[];
36
- error?: Array<{
37
- code: number;
38
- msg: string;
39
- }>;
41
+ error?: KagiErrorEntry[];
42
+ }
43
+
44
+ interface KagiErrorResponse {
45
+ error?: string | KagiErrorEntry[];
46
+ message?: string;
47
+ detail?: string;
40
48
  }
41
49
 
42
50
  export class KagiApiError extends Error {
@@ -49,6 +57,54 @@ export class KagiApiError extends Error {
49
57
  }
50
58
  }
51
59
 
60
+ function extractKagiErrorMessage(payload: unknown): string | null {
61
+ if (!payload || typeof payload !== "object") return null;
62
+ const record = payload as Record<string, unknown>;
63
+
64
+ for (const value of [record.message, record.detail]) {
65
+ if (typeof value === "string" && value.trim().length > 0) {
66
+ return value.trim();
67
+ }
68
+ }
69
+
70
+ if (typeof record.error === "string" && record.error.trim().length > 0) {
71
+ return record.error.trim();
72
+ }
73
+
74
+ if (Array.isArray(record.error)) {
75
+ for (const entry of record.error) {
76
+ if (!entry || typeof entry !== "object") continue;
77
+ const message = (entry as Record<string, unknown>).msg;
78
+ if (typeof message === "string" && message.trim().length > 0) {
79
+ return message.trim();
80
+ }
81
+ }
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ function createKagiApiError(statusCode: number, detail?: string): KagiApiError {
88
+ return new KagiApiError(
89
+ detail ? `Kagi API error (${statusCode}): ${detail}` : `Kagi API error (${statusCode})`,
90
+ statusCode,
91
+ );
92
+ }
93
+
94
+ function parseKagiErrorResponse(statusCode: number, responseText: string): KagiApiError {
95
+ const trimmedResponseText = responseText.trim();
96
+ if (trimmedResponseText.length === 0) {
97
+ return createKagiApiError(statusCode);
98
+ }
99
+
100
+ try {
101
+ const payload = JSON.parse(trimmedResponseText) as KagiErrorResponse;
102
+ return createKagiApiError(statusCode, extractKagiErrorMessage(payload) ?? trimmedResponseText);
103
+ } catch {
104
+ return createKagiApiError(statusCode, trimmedResponseText);
105
+ }
106
+ }
107
+
52
108
  export interface KagiSummarizeOptions {
53
109
  engine?: string;
54
110
  summaryType?: string;
@@ -127,14 +183,13 @@ export async function searchWithKagi(query: string, options: KagiSearchOptions =
127
183
  signal: options.signal,
128
184
  });
129
185
  if (!response.ok) {
130
- const errorText = await response.text();
131
- throw new KagiApiError(`Kagi API error (${response.status}): ${errorText}`, response.status);
186
+ throw parseKagiErrorResponse(response.status, await response.text());
132
187
  }
133
188
 
134
189
  const payload = (await response.json()) as KagiSearchResponse;
135
190
  if (payload.error && payload.error.length > 0) {
136
191
  const firstError = payload.error[0];
137
- throw new KagiApiError(`Kagi API error: ${firstError.msg}`, firstError.code);
192
+ throw createKagiApiError(firstError.code ?? response.status, extractKagiErrorMessage(payload) ?? undefined);
138
193
  }
139
194
 
140
195
  const sources: KagiSearchSource[] = [];