@oh-my-pi/pi-ai 18.12.0 → 18.12.2

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,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.12.2] - 2026-10-18
6
+
7
+ ### Added
8
+
9
+ - Added `nativeJudgmentImageBounds()`: the image bounds a native judge enforces, its wire's transport limits tightened by the catalog's model bounds
10
+ - Added Box judgment questions (`BoxQuestion`/`BoxAnswer`, TypeSafe Jev 1.14+ direct or through OpenRouter): each names the request image it searches by `image` index, and comes back as that image's edges as 0–1 fractions; text, OpenAI Decisions, and Workers AI judges reject them
11
+
12
+ ### Fixed
13
+
14
+ - Fixed Muse Code usage going stale whenever its 5-hour window sits idle: the idle window now reports as unused, and weekly usage carries over from the last active window until its reset ([#16193](https://github.com/can1357/oh-my-pi/pull/16193) by [@eggpeat](https://github.com/eggpeat))
15
+ - Fixed DeepSeek tool calls never running when the model writes them as chat-template tokens with `_` in place of `▁`, a missing `|`, or no per-call close (e.g. `<|tool_calls_begin|>…<|tool_calls_end|>`): they now run as tool calls, and any that still cannot be parsed are removed from history and reported to the model as a failed call ([#16216](https://github.com/can1357/oh-my-pi/pull/16216) by [@H4vC](https://github.com/H4vC))
16
+
17
+ ## [18.12.1] - 2026-10-17
18
+
19
+ ### Fixed
20
+
21
+ - Fixed every request failing behind a TLS-inspecting proxy that refuses compressed uploads: a 400/403/415 whose body is not JSON, or the Anthropic API's "request body is not valid JSON" after a proxy drops `Content-Encoding`, now resends the body uncompressed and stops compressing for that host ([#16108](https://github.com/can1357/oh-my-pi/pull/16108) by [@alphastorm](https://github.com/alphastorm))
22
+
5
23
  ## [18.12.0] - 2026-10-17
6
24
 
7
25
  ### Breaking Changes
@@ -2336,25 +2354,4 @@
2336
2354
 
2337
2355
  - Removed the `partial-json` dependency; streaming JSON parsing now uses the in-house `RelaxedJson` parser.
2338
2356
 
2339
- ## [16.1.9] - 2026-06-21
2340
-
2341
- ### Added
2342
-
2343
- - Added `llama.cpp` to the interactive `/login` provider list, accepting an optional API key while defaulting to local no-auth mode.
2344
-
2345
- ### Changed
2346
-
2347
- - Optimized generated AI tool schemas by collapsing verbose `anyOf` unions into standard `enum` types
2348
-
2349
- ### Fixed
2350
-
2351
- - Fixed tool-call argument validation dropping nested keys that were accidentally double-encoded
2352
- - Fixed the `moonshot` provider being locked to the international Kimi host (`api.moonshot.ai`): OpenAI-completions requests now honor a `MOONSHOT_BASE_URL` override so users can reach the Kimi China platform (`api.moonshot.cn`), which rejects keys issued for the international endpoint. ([#2883](https://github.com/can1357/oh-my-pi/issues/2883))
2353
- - Fixed tool-call argument validation dropping fields whose object keys were accidentally JSON-encoded a second time (e.g. `{ "\"op\"": "done" }`), which surfaced as spurious missing-required errors. A schema-agnostic pre-validation pass now recursively unwraps such double-encoded keys — through arrays and nested objects, and again after a JSON-string container is parsed — before the unrecognized-key repair can delete them.
2354
-
2355
- ### Removed
2356
-
2357
- - Removed the `setNextRequestDebugPath`, `clearNextRequestDebugPath`, and `getNextRequestDebugPath` utility functions for request debugging, as request/response recording now relies exclusively on the `PI_REQ_DEBUG` environment variable.
2358
- - Removed Wafer Pass (`wafer-pass`) login support; Wafer Serverless remains available as `wafer-serverless`.
2359
-
2360
- Older entries are archived in [packages/ai/CHANGELOG.md@50ec66f75d57](https://github.com/can1357/oh-my-pi/blob/50ec66f75d57ad55838a9fe4da5e66f29af4d73c/packages/ai/CHANGELOG.md).
2357
+ Older entries are archived in [packages/ai/CHANGELOG.md@bb602d67c9b6](https://github.com/can1357/oh-my-pi/blob/bb602d67c9b685535bbf8ef1a1cc9de3c944ede4/packages/ai/CHANGELOG.md).
@@ -4,6 +4,19 @@ export declare const DEEPSEEK_TOOL_CALLS_END = "<\uFF5Ctool\u2581calls\u2581end\
4
4
  export declare const DEEPSEEK_TOOL_CALL_BEGIN = "<\uFF5Ctool\u2581call\u2581begin\uFF5C>";
5
5
  export declare const DEEPSEEK_TOOL_CALL_END = "<\uFF5Ctool\u2581call\u2581end\uFF5C>";
6
6
  export declare const DEEPSEEK_TOOL_SEPARATOR = "<\uFF5Ctool\u2581sep\uFF5C>";
7
+ /** DeepSeek template tokens that structure a tool call. */
8
+ export declare const DEEPSEEK_TOOL_CALL_TOKENS: Readonly<Record<string, true>>;
9
+ /** A token found in text: its canonical spelling and the text actually written. */
10
+ export interface DeepSeekTokenMatch {
11
+ readonly token: string;
12
+ readonly raw: string;
13
+ readonly index: number;
14
+ }
15
+ /**
16
+ * Find the earliest of `tokens` in `text` at or after `from`, in its canonical
17
+ * spelling or any re-spelling described at {@link TEMPLATE_TOKEN_SPELLING}.
18
+ */
19
+ export declare function findDeepSeekTemplateToken(text: string, tokens: Readonly<Record<string, true>>, from?: number): DeepSeekTokenMatch | undefined;
7
20
  export declare class DeepSeekInbandScanner implements InbandScanner {
8
21
  #private;
9
22
  constructor(options?: InbandScannerOptions);
@@ -41,10 +41,15 @@ export type JudgmentApi = keyof typeof JUDGMENT_WIRES;
41
41
  export declare function isJudgmentApi(api: Api): api is JudgmentApi;
42
42
  /** Whether both the native protocol and catalog model support image input. */
43
43
  export declare function nativeJudgmentSupportsImages(model: Model): boolean;
44
+ /** Whether `model`'s native judgment takes images and its wire forwards box questions. The model itself may still refuse them (Box needs Jev 1.14+). */
45
+ export declare function nativeJudgmentSupportsBoxes(model: Model): boolean;
44
46
  /**
45
- * Whether `model`'s native judgment API takes these exact images: image support plus every documented
46
- * transport and catalog bound. A judge chain skips a candidate that would refuse them instead of failing on it.
47
+ * Every image bound `model`'s native judgment enforces: its wire's transport limits tightened by the
48
+ * catalog's model bounds. `undefined` when it takes no images. Native APIs reject images outside these
49
+ * rather than resizing them, so callers size images to them first.
47
50
  */
51
+ export declare function nativeJudgmentImageBounds(model: Model): JudgmentImageInput | undefined;
52
+ /** Whether `model`'s native judgment API takes these exact images: image support within every {@link nativeJudgmentImageBounds}. */
48
53
  export declare function nativeJudgmentAcceptsImages(model: Model, images: readonly ImageContent[]): boolean;
49
54
  /**
50
55
  * Reject images that `model`'s native judgment API cannot take — no image support, an unsupported or
@@ -52,6 +57,12 @@ export declare function nativeJudgmentAcceptsImages(model: Model, images: readon
52
57
  * @throws {AIError.ValidationError} naming the reason.
53
58
  */
54
59
  export declare function checkNativeJudgmentImages(model: Model, request: JudgmentRequest, imageAssets?: JudgmentImageAssets): void;
60
+ /**
61
+ * Reject box questions whose `image` is not an index into `request.images`: nothing would say which
62
+ * image their edges measure.
63
+ * @throws {AIError.ValidationError} naming the question.
64
+ */
65
+ export declare function checkBoxQuestions(request: JudgmentRequest): void;
55
66
  /** `TYPESAFE_BASE_URL` when set, else the public API root; trailing slashes stripped. */
56
67
  export declare function typesafeBaseUrl(): string;
57
68
  /** `TYPESAFE_DEFAULT_MODEL` when set, else {@link TYPESAFE_DEFAULT_MODEL}. */
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * A {@link Judge} answers a map of named questions — {@link ChoiceQuestion}
5
5
  * (one option from a fixed set), {@link NoulQuestion} (probability that a
6
- * yes/no condition holds), {@link ScoreQuestion} (position on ordered levels)
7
- * — about one {@link JudgmentState}. Every question in a request sees the same
6
+ * yes/no condition holds), {@link ScoreQuestion} (position on ordered levels),
7
+ * {@link BoxQuestion} (where a target is in one of the request's images, native
8
+ * judges only) — about one {@link JudgmentState}. Every question in a request sees the same
8
9
  * state and is answered independently, so callers batch independent questions
9
10
  * into one call. The shape mirrors TypeSafe's System One API so the native
10
11
  * backend ({@link NativeJudge}) forwards requests verbatim, while the text
@@ -41,7 +42,19 @@ export interface ScoreQuestion {
41
42
  instructions: string;
42
43
  criteria: readonly [string, string, ...string[]];
43
44
  }
44
- export type Question = ChoiceQuestion | NoulQuestion | ScoreQuestion;
45
+ /**
46
+ * Where one target is in an image (TypeSafe Box, Jev 1.14+): https://docs.typesafe.ai/primitives/box.
47
+ * Only native judges answer it. `instructions` must name the target and, among several images, which
48
+ * one to search; `image` stays local and says which entry of {@link JudgmentRequest.images} the
49
+ * answer's edges measure, so a judge that resizes or pads that image can map them back.
50
+ */
51
+ export interface BoxQuestion {
52
+ type: "box";
53
+ instructions: string;
54
+ /** Index into the request's `images` of the image searched. */
55
+ image: number;
56
+ }
57
+ export type Question = ChoiceQuestion | NoulQuestion | ScoreQuestion | BoxQuestion;
45
58
  /** Questions keyed by caller-chosen ids; answers come back under the same ids. */
46
59
  export type Questions = Record<string, Question>;
47
60
  export interface ChoiceAnswer<L extends string = string> {
@@ -67,9 +80,23 @@ export interface ScoreAnswer {
67
80
  /** Concentration of the distribution, 0–1. */
68
81
  confidence: number;
69
82
  }
70
- export type Answer = ChoiceAnswer | NoulAnswer | ScoreAnswer;
83
+ /** A rectangle's edges as 0–1 fractions of an image's width (`left` < `right`) and height (`top` < `bottom`), from its top-left corner. */
84
+ export interface BoxEdges {
85
+ top: number;
86
+ bottom: number;
87
+ left: number;
88
+ right: number;
89
+ }
90
+ export interface BoxAnswer {
91
+ type: "box";
92
+ /** Smallest rectangle containing the target, on the image the question's `image` names. Always present: absence is a separate yes/no question. */
93
+ box: BoxEdges;
94
+ /** How sure the model is of the box, 0–1. */
95
+ confidence: number;
96
+ }
97
+ export type Answer = ChoiceAnswer | NoulAnswer | ScoreAnswer | BoxAnswer;
71
98
  /** The answer type for a question, preserving choice labels. */
72
- export type AnswerFor<Q extends Question> = Q extends ChoiceQuestion<infer L> ? ChoiceAnswer<L> : Q extends NoulQuestion ? NoulAnswer : ScoreAnswer;
99
+ export type AnswerFor<Q extends Question> = Q extends ChoiceQuestion<infer L> ? ChoiceAnswer<L> : Q extends NoulQuestion ? NoulAnswer : Q extends BoxQuestion ? BoxAnswer : ScoreAnswer;
73
100
  export interface JudgmentRequest<Q extends Questions = Questions> {
74
101
  state: JudgmentState;
75
102
  questions: Q;
@@ -42,6 +42,8 @@ export interface JudgmentWire {
42
42
  imagesRequireOptIn?: boolean;
43
43
  /** Whether this wire drops per-image detail metadata. */
44
44
  ignoresImageDetail?: boolean;
45
+ /** Whether this dialect forwards `box` questions; others reject them before sending. */
46
+ boxes?: boolean;
45
47
  /** The response's answers and usage; `undefined` when the payload is not this API's response shape. */
46
48
  decode(payload: unknown): DecodedJudgment | undefined;
47
49
  }
@@ -1,8 +1,13 @@
1
- import type { StreamFunction, StreamOptions, ToolChoice } from "../types.js";
1
+ import type { Model, StreamFunction, StreamOptions, ToolChoice } from "../types.js";
2
2
  export interface OllamaChatOptions extends StreamOptions {
3
3
  reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
4
4
  disableReasoning?: boolean;
5
5
  toolChoice?: ToolChoice;
6
6
  }
7
+ /** Whether Ollama chat removes assistant thinking from replayed history. */
8
+ export declare function ollamaChatDropsOwnThinking(model: {
9
+ api?: string;
10
+ compat?: Model["compat"];
11
+ } | null | undefined): boolean;
7
12
  /** Retry EOS-only Ollama completions before the agent loop sees an empty stop. */
8
13
  export declare const streamOllama: StreamFunction<"ollama-chat">;
@@ -52,6 +52,8 @@ export declare function shouldDropTruncatedThinkingOnlyAssistant(msg: AssistantM
52
52
  */
53
53
  export declare function isSurvivingAssistant(msg: AssistantMessage, model?: Model): boolean;
54
54
  export declare function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messages">;
55
+ /** Whether `turn` came from `model` itself: its own turns replay their native reasoning unchanged. */
56
+ export declare function isSameModelTurn(turn: Pick<AssistantMessage, "provider" | "api" | "model">, model: Pick<Model, "provider" | "api" | "id">): boolean;
55
57
  /**
56
58
  * Normalize tool call ID for cross-provider compatibility.
57
59
  * OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`.
@@ -1,2 +1,2 @@
1
- import type { UsageProvider } from "../usage.js";
1
+ import { type UsageProvider } from "../usage.js";
2
2
  export declare const museCodeUsageProvider: UsageProvider;
@@ -1,25 +1,27 @@
1
1
  /**
2
- * Removal of DeepSeek DSML tool-call markup left in visible assistant text.
2
+ * Removal of DeepSeek tool-call markup left in visible assistant text.
3
3
  *
4
4
  * DeepSeek models on OpenAI-compatible hosts sometimes emit their tool calls as
5
- * raw `<|DSML|…>` markup in `content`. A well-formed envelope is healed into a
6
- * structured call by `StreamMarkupHealing`, but a degraded model can drop the
7
- * opening `<|DSML|tool_calls>` / `<|DSML|invoke name="…">` tags (or the host
8
- * runs no healer at all). That markup must not stay in history: replaying it
9
- * teaches the model to keep writing tool calls as text (issue #10556). Callers
10
- * strip it before the message is committed and tell the model its call failed.
5
+ * text in `content`: the V4 `<|DSML|…>` envelope or the V3 chat-template
6
+ * tokens (`<|tool▁calls▁begin|>` …). A well-formed call is healed into a
7
+ * structured call by `StreamMarkupHealing`, but a degraded model can drop
8
+ * required tags (or the host runs no healer at all). That markup must not stay
9
+ * in history: replaying it teaches the model to keep writing tool calls as text
10
+ * (issue #10556). Callers strip it before the message is committed and tell the
11
+ * model its call failed.
11
12
  */
12
13
  import type { AssistantMessage, Model } from "../types.js";
13
14
  /**
14
15
  * Whether DSML leak recovery applies to `model`: a DeepSeek-class model, or one
15
16
  * explicitly configured with the `dsml` stream healer (e.g. a `models.yml` entry
16
- * whose id the classifier cannot place). Other models never write DSML tool
17
- * calls, so DSML text from them is prose and is left alone.
17
+ * whose id the classifier cannot place). Other models never write DeepSeek tool
18
+ * calls, so such markup from them is prose and is left alone.
18
19
  */
19
20
  export declare function isDsmlLeakRecoveryTarget(model: Model): boolean;
20
21
  /**
21
- * Strip leaked DSML tool-call markup from `text`, leaving quoted code intact.
22
- * Returns `undefined` when `text` carries no such markup.
22
+ * Strip leaked DeepSeek tool-call markup (DSML or chat-template tokens) from
23
+ * `text`, leaving quoted code intact. Returns `undefined` when `text` carries
24
+ * no such markup.
23
25
  */
24
26
  export declare function stripDsmlToolMarkup(text: string): string | undefined;
25
27
  /**
@@ -9,10 +9,13 @@ import type { FetchImpl, Model } from "../types.js";
9
9
  * Install it beneath any body rewriter or signer (e.g. the cch attestation
10
10
  * patch): those must see the uncompressed bytes. Other hosts, small bodies,
11
11
  * bodies that already carry `Content-Encoding`, and `PI_REQUEST_COMPRESSION=0`
12
- * pass through untouched. A possible rejection is resent uncompressed once;
13
- * only when that resend succeeds is the encoding latched off for the host, so
14
- * a genuine 400 never disables compression.
12
+ * pass through untouched. A possible rejection — the host's decode error, or a
13
+ * non-JSON 400/403/415 from a proxy in front of it — is resent uncompressed
14
+ * once; only when that resend succeeds is the encoding latched off for the
15
+ * host, so a genuine 400 never disables compression. When a non-JSON refusal
16
+ * recurs uncompressed with the same status and a non-JSON body, that status
17
+ * stops being re-checked for the host.
15
18
  */
16
19
  export declare function wrapFetchForRequestCompression(base: FetchImpl, model: Model): FetchImpl;
17
- /** Test hook: clears every per-host rejection latch. */
20
+ /** Test hook: clears every per-host latch. */
18
21
  export declare function resetRequestCompressionForTesting(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.12.0",
3
+ "version": "18.12.2",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -155,11 +155,11 @@
155
155
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
156
156
  },
157
157
  "dependencies": {
158
- "@oh-my-pi/omptype": "18.12.0",
159
- "@oh-my-pi/pi-catalog": "18.12.0",
160
- "@oh-my-pi/pi-natives": "18.12.0",
161
- "@oh-my-pi/pi-utils": "18.12.0",
162
- "@oh-my-pi/pi-wire": "18.12.0"
158
+ "@oh-my-pi/omptype": "18.12.2",
159
+ "@oh-my-pi/pi-catalog": "18.12.2",
160
+ "@oh-my-pi/pi-natives": "18.12.2",
161
+ "@oh-my-pi/pi-utils": "18.12.2",
162
+ "@oh-my-pi/pi-wire": "18.12.2"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"