@oh-my-pi/pi-ai 16.1.19 → 16.1.21
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 -0
- package/dist/types/registry/oauth/index.d.ts +25 -0
- package/package.json +4 -4
- package/src/providers/ollama.ts +20 -0
- package/src/registry/oauth/index.ts +88 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.21] - 2026-06-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Restored the `pollOAuthDeviceCodeFlow` export from `@oh-my-pi/pi-ai/oauth` so legacy provider extensions can reuse the host OAuth device-code poller. ([#3508](https://github.com/can1357/oh-my-pi/issues/3508))
|
|
10
|
+
|
|
11
|
+
## [16.1.20] - 2026-06-25
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed Ollama/Ollama Cloud native chat responses that finish with `done_reason: "length"` and no assistant content surfacing as a normal empty stop; they now become a context-window error instead of entering empty-stop retry recovery. ([#3464](https://github.com/can1357/oh-my-pi/issues/3464))
|
|
16
|
+
- Fixed direct Anthropic Claude Sonnet/Haiku 4.5 requests serializing `output_config.effort`. The catalog classification (`packages/catalog/src/model-thinking.ts`) drove the `anthropic-budget-effort` branch in `buildParams`, which Anthropic's first-party Messages API rejects on Sonnet/Haiku 4.5 with HTTP 400 `This model does not support the effort parameter.` Sonnet/Haiku 4.5 now use plain `thinking.budget_tokens`; Opus 4.5 still emits `output_config.effort` because Anthropic supports it there. ([#3497](https://github.com/can1357/oh-my-pi/issues/3497))
|
|
17
|
+
|
|
5
18
|
## [16.1.19] - 2026-06-25
|
|
6
19
|
|
|
7
20
|
### Fixed
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import type { OAuthCredentials, OAuthProvider, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types";
|
|
2
2
|
export type * from "./types";
|
|
3
|
+
/** Result returned by one OAuth device-code polling attempt. */
|
|
4
|
+
export type OAuthDeviceCodePollResult<T> = {
|
|
5
|
+
status: "complete";
|
|
6
|
+
value: T;
|
|
7
|
+
} | {
|
|
8
|
+
status: "pending";
|
|
9
|
+
} | {
|
|
10
|
+
status: "slow_down";
|
|
11
|
+
} | {
|
|
12
|
+
status: "failed";
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
/** Options for polling an RFC 8628-style OAuth device-code flow. */
|
|
16
|
+
export interface OAuthDeviceCodeFlowOptions<T> {
|
|
17
|
+
/** Poll the provider once and classify the response. */
|
|
18
|
+
poll(): OAuthDeviceCodePollResult<T> | Promise<OAuthDeviceCodePollResult<T>>;
|
|
19
|
+
/** Provider-requested polling cadence; defaults to RFC 8628's five seconds. */
|
|
20
|
+
intervalSeconds?: number;
|
|
21
|
+
/** Provider-issued expiry window for the device code. */
|
|
22
|
+
expiresInSeconds?: number;
|
|
23
|
+
/** Cancels the flow with the legacy "Login cancelled" error. */
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
/** Poll an OAuth device-code flow until completion, provider failure, timeout, or cancellation. */
|
|
27
|
+
export declare function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodeFlowOptions<T>): Promise<T>;
|
|
3
28
|
/**
|
|
4
29
|
* Register a custom OAuth provider.
|
|
5
30
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.21",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.1.21",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.1.21",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.1.21",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
package/src/providers/ollama.ts
CHANGED
|
@@ -437,6 +437,17 @@ function mapDoneReason(doneReason: string | undefined, output: AssistantMessage)
|
|
|
437
437
|
return "stop";
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
const EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE =
|
|
441
|
+
"Model returned no content: prompt filled the context window; raise Ollama num_ctx or shorten the prompt.";
|
|
442
|
+
|
|
443
|
+
function hasVisibleAssistantContent(output: AssistantMessage): boolean {
|
|
444
|
+
return output.content.some(block => {
|
|
445
|
+
if (block.type === "text") return block.text.trim().length > 0;
|
|
446
|
+
if (block.type === "thinking") return block.thinking.trim().length > 0;
|
|
447
|
+
return block.type === "toolCall";
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
440
451
|
const OLLAMA_RETRY_DELAYS_MS = [2_000, 5_000, 10_000];
|
|
441
452
|
|
|
442
453
|
export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
@@ -702,6 +713,10 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
702
713
|
}
|
|
703
714
|
endActiveThinkingBlock();
|
|
704
715
|
endActiveTextBlock();
|
|
716
|
+
if (output.stopReason === "length" && !hasVisibleAssistantContent(output)) {
|
|
717
|
+
output.stopReason = "error";
|
|
718
|
+
output.errorMessage = EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE;
|
|
719
|
+
}
|
|
705
720
|
// Tool calls always mean "execute and continue" in the OpenAI/Ollama contract.
|
|
706
721
|
// If the turn produced tool-call blocks but reported a natural `stop`, promote
|
|
707
722
|
// to `toolUse` so the agent loop runs them (it gates execution on the stop
|
|
@@ -713,6 +728,11 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
713
728
|
if (firstTokenTime) {
|
|
714
729
|
output.ttft = firstTokenTime - startTime;
|
|
715
730
|
}
|
|
731
|
+
if (output.stopReason === "error") {
|
|
732
|
+
stream.push({ type: "error", reason: "error", error: output });
|
|
733
|
+
stream.end();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
716
736
|
const doneReason =
|
|
717
737
|
output.stopReason === "length" ? "length" : output.stopReason === "toolUse" ? "toolUse" : "stop";
|
|
718
738
|
stream.push({ type: "done", reason: doneReason, message: output });
|
|
@@ -13,6 +13,94 @@ import type {
|
|
|
13
13
|
|
|
14
14
|
export type * from "./types";
|
|
15
15
|
|
|
16
|
+
const DEVICE_FLOW_CANCEL_MESSAGE = "Login cancelled";
|
|
17
|
+
const DEVICE_FLOW_TIMEOUT_MESSAGE = "Device flow timed out";
|
|
18
|
+
const DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE =
|
|
19
|
+
"Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";
|
|
20
|
+
const MINIMUM_DEVICE_FLOW_INTERVAL_MS = 1000;
|
|
21
|
+
const DEFAULT_DEVICE_FLOW_INTERVAL_SECONDS = 5;
|
|
22
|
+
const SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;
|
|
23
|
+
|
|
24
|
+
/** Result returned by one OAuth device-code polling attempt. */
|
|
25
|
+
export type OAuthDeviceCodePollResult<T> =
|
|
26
|
+
| { status: "complete"; value: T }
|
|
27
|
+
| { status: "pending" }
|
|
28
|
+
| { status: "slow_down" }
|
|
29
|
+
| { status: "failed"; message: string };
|
|
30
|
+
|
|
31
|
+
/** Options for polling an RFC 8628-style OAuth device-code flow. */
|
|
32
|
+
export interface OAuthDeviceCodeFlowOptions<T> {
|
|
33
|
+
/** Poll the provider once and classify the response. */
|
|
34
|
+
poll(): OAuthDeviceCodePollResult<T> | Promise<OAuthDeviceCodePollResult<T>>;
|
|
35
|
+
/** Provider-requested polling cadence; defaults to RFC 8628's five seconds. */
|
|
36
|
+
intervalSeconds?: number;
|
|
37
|
+
/** Provider-issued expiry window for the device code. */
|
|
38
|
+
expiresInSeconds?: number;
|
|
39
|
+
/** Cancels the flow with the legacy "Login cancelled" error. */
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function abortableDeviceFlowSleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
|
|
44
|
+
if (!signal) {
|
|
45
|
+
await Bun.sleep(ms);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (signal.aborted) {
|
|
49
|
+
throw new Error(DEVICE_FLOW_CANCEL_MESSAGE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
53
|
+
let timer: Timer | undefined;
|
|
54
|
+
const onAbort = () => {
|
|
55
|
+
if (timer) clearTimeout(timer);
|
|
56
|
+
reject(new Error(DEVICE_FLOW_CANCEL_MESSAGE));
|
|
57
|
+
};
|
|
58
|
+
timer = setTimeout(() => {
|
|
59
|
+
signal.removeEventListener("abort", onAbort);
|
|
60
|
+
resolve();
|
|
61
|
+
}, ms);
|
|
62
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
63
|
+
await promise;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Poll an OAuth device-code flow until completion, provider failure, timeout, or cancellation. */
|
|
67
|
+
export async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodeFlowOptions<T>): Promise<T> {
|
|
68
|
+
const deadline =
|
|
69
|
+
typeof options.expiresInSeconds === "number"
|
|
70
|
+
? Date.now() + options.expiresInSeconds * 1000
|
|
71
|
+
: Number.POSITIVE_INFINITY;
|
|
72
|
+
let intervalMs = Math.max(
|
|
73
|
+
MINIMUM_DEVICE_FLOW_INTERVAL_MS,
|
|
74
|
+
Math.floor((options.intervalSeconds ?? DEFAULT_DEVICE_FLOW_INTERVAL_SECONDS) * 1000),
|
|
75
|
+
);
|
|
76
|
+
let slowDownResponses = 0;
|
|
77
|
+
|
|
78
|
+
while (Date.now() < deadline) {
|
|
79
|
+
if (options.signal?.aborted) {
|
|
80
|
+
throw new Error(DEVICE_FLOW_CANCEL_MESSAGE);
|
|
81
|
+
}
|
|
82
|
+
const result = await options.poll();
|
|
83
|
+
if (result.status === "complete") {
|
|
84
|
+
return result.value;
|
|
85
|
+
}
|
|
86
|
+
if (result.status === "failed") {
|
|
87
|
+
throw new Error(result.message);
|
|
88
|
+
}
|
|
89
|
+
if (result.status === "slow_down") {
|
|
90
|
+
slowDownResponses += 1;
|
|
91
|
+
intervalMs = Math.max(MINIMUM_DEVICE_FLOW_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const remainingMs = deadline - Date.now();
|
|
95
|
+
if (remainingMs <= 0) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
await abortableDeviceFlowSleep(Math.min(intervalMs, remainingMs), options.signal);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new Error(slowDownResponses > 0 ? DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE : DEVICE_FLOW_TIMEOUT_MESSAGE);
|
|
102
|
+
}
|
|
103
|
+
|
|
16
104
|
const builtInOAuthProviders: OAuthProviderInfo[] = PROVIDER_REGISTRY.filter(
|
|
17
105
|
provider => provider.login && provider.showInLoginList !== false,
|
|
18
106
|
).map(provider => ({
|