@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 +11 -0
- package/package.json +7 -7
- package/src/cli/args.ts +4 -1
- package/src/config/model-registry.ts +74 -22
- package/src/discovery/helpers.ts +1 -1
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/components/status-line.ts +2 -6
- package/src/sdk.ts +2 -2
- package/src/session/agent-session.ts +2 -1
- package/src/tools/index.ts +2 -1
- package/src/web/kagi.ts +62 -7
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.
|
|
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.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.9.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.9.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.9.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.9.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.9.
|
|
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]
|
|
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
|
|
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
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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", {
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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]);
|
package/src/tools/index.ts
CHANGED
|
@@ -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 =
|
|
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?:
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
192
|
+
throw createKagiApiError(firstError.code ?? response.status, extractKagiErrorMessage(payload) ?? undefined);
|
|
138
193
|
}
|
|
139
194
|
|
|
140
195
|
const sources: KagiSearchSource[] = [];
|