pi-free 1.0.9 → 2.0.1
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 +138 -0
- package/README.md +393 -367
- package/config.ts +170 -121
- package/constants.ts +23 -61
- package/index.ts +148 -0
- package/lib/built-in-toggle.ts +206 -0
- package/lib/json-persistence.ts +11 -10
- package/lib/logger.ts +2 -2
- package/lib/model-enhancer.ts +20 -20
- package/lib/open-browser.ts +41 -0
- package/lib/provider-cache.ts +106 -0
- package/lib/registry.ts +144 -0
- package/package.json +8 -23
- package/provider-factory.ts +25 -41
- package/provider-failover/benchmark-lookup.ts +247 -0
- package/provider-failover/benchmarks-chunk-0.ts +2010 -0
- package/provider-failover/benchmarks-chunk-1.ts +1988 -0
- package/provider-failover/benchmarks-chunk-2.ts +2010 -0
- package/provider-failover/benchmarks-chunk-3.ts +2010 -0
- package/provider-failover/benchmarks-chunk-4.ts +1969 -0
- package/provider-failover/hardcoded-benchmarks.ts +22 -10025
- package/provider-helper.ts +260 -259
- package/providers/{cline-auth.ts → cline/cline-auth.ts} +2 -2
- package/providers/cline/cline-models.ts +128 -0
- package/providers/{cline.ts → cline/cline.ts} +298 -257
- package/providers/cloudflare/cloudflare.ts +368 -0
- package/providers/dynamic-built-in/index.ts +432 -0
- package/providers/{kilo-auth.ts → kilo/kilo-auth.ts} +3 -20
- package/providers/{kilo-models.ts → kilo/kilo-models.ts} +2 -2
- package/providers/kilo/kilo.ts +235 -0
- package/providers/{modal.ts → modal/modal.ts} +4 -3
- package/providers/{nvidia.ts → nvidia/nvidia.ts} +152 -113
- package/providers/ollama/ollama.ts +172 -0
- package/providers/opencode-session.ts +34 -34
- package/providers/{qwen-auth.ts → qwen/qwen-auth.ts} +24 -40
- package/providers/{qwen-models.ts → qwen/qwen-models.ts} +101 -95
- package/providers/{qwen.ts → qwen/qwen.ts} +83 -13
- package/provider-failover/auto-switch.ts +0 -350
- package/provider-failover/errors.ts +0 -275
- package/provider-failover/index.ts +0 -238
- package/providers/cline-models.ts +0 -77
- package/providers/factory.ts +0 -125
- package/providers/fireworks.ts +0 -49
- package/providers/go.ts +0 -216
- package/providers/kilo.ts +0 -146
- package/providers/mistral.ts +0 -144
- package/providers/ollama.ts +0 -113
- package/providers/openrouter.ts +0 -175
- package/providers/zen.ts +0 -371
- package/usage/commands.ts +0 -17
- package/usage/cumulative.ts +0 -193
- package/usage/formatters.ts +0 -115
- package/usage/index.ts +0 -46
- package/usage/limits.ts +0 -148
- package/usage/metrics.ts +0 -222
- package/usage/sessions.ts +0 -355
- package/usage/store.ts +0 -99
- package/usage/tracking.ts +0 -329
- package/usage/types.ts +0 -26
- package/usage/widget.ts +0 -90
- package/widget/data.ts +0 -113
- package/widget/format.ts +0 -26
- package/widget/render.ts +0 -117
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
|
|
11
|
-
import { applyHidden, PROVIDER_MODAL } from "
|
|
12
|
-
import { BASE_URL_MODAL, URL_MODAL_TOS } from "
|
|
13
|
-
import { createProvider } from "
|
|
11
|
+
import { applyHidden, PROVIDER_MODAL } from "../../config.ts";
|
|
12
|
+
import { BASE_URL_MODAL, URL_MODAL_TOS } from "../../constants.ts";
|
|
13
|
+
import { createProvider } from "../../provider-factory.ts";
|
|
14
14
|
|
|
15
15
|
function getModalModels(): ProviderModelConfig[] {
|
|
16
16
|
return applyHidden([
|
|
@@ -35,6 +35,7 @@ export default function (pi: Parameters<typeof createProvider>[0]) {
|
|
|
35
35
|
apiKeyConfigKey: "modal_api_key",
|
|
36
36
|
fetchModels: async () => getModalModels(),
|
|
37
37
|
tosUrl: URL_MODAL_TOS,
|
|
38
|
+
skipToggle: true, // Modal only has 1 model, no need for toggle
|
|
38
39
|
extraHeaders: {
|
|
39
40
|
"X-Title": "Pi",
|
|
40
41
|
"HTTP-Referer": "https://modal.com/",
|
|
@@ -1,113 +1,152 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NVIDIA NIM Provider Extension
|
|
3
|
-
*
|
|
4
|
-
* Provides access to NVIDIA-hosted large models via integrate.api.nvidia.com.
|
|
5
|
-
* All models use NVIDIA's free credit system — requires NVIDIA_API_KEY.
|
|
6
|
-
* Get a free key at: https://build.nvidia.com
|
|
7
|
-
*
|
|
8
|
-
* Small models (< 70B) are filtered out to keep the list focused on useful
|
|
9
|
-
* chat/coding models. Non-chat models (embedding, speech-to-text, OCR,
|
|
10
|
-
* image-gen) are filtered by their modalities (output must be ["text"],
|
|
11
|
-
* input must include "text").
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
/**
|
|
2
|
+
* NVIDIA NIM Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides access to NVIDIA-hosted large models via integrate.api.nvidia.com.
|
|
5
|
+
* All models use NVIDIA's free credit system — requires NVIDIA_API_KEY.
|
|
6
|
+
* Get a free key at: https://build.nvidia.com
|
|
7
|
+
*
|
|
8
|
+
* Small models (< 70B) are filtered out to keep the list focused on useful
|
|
9
|
+
* chat/coding models. Non-chat models (embedding, speech-to-text, OCR,
|
|
10
|
+
* image-gen) are filtered by their modalities (output must be ["text"],
|
|
11
|
+
* input must include "text").
|
|
12
|
+
*
|
|
13
|
+
* Responds to global free-only filter for free/paid model filtering.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
ExtensionAPI,
|
|
18
|
+
ProviderModelConfig,
|
|
19
|
+
} from "@mariozechner/pi-coding-agent";
|
|
20
|
+
import {
|
|
21
|
+
applyHidden,
|
|
22
|
+
getNvidiaApiKey,
|
|
23
|
+
getNvidiaShowPaid,
|
|
24
|
+
PROVIDER_NVIDIA,
|
|
25
|
+
} from "../../config.ts";
|
|
26
|
+
import {
|
|
27
|
+
BASE_URL_NVIDIA,
|
|
28
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
29
|
+
NVIDIA_MIN_SIZE_B,
|
|
30
|
+
URL_MODELS_DEV,
|
|
31
|
+
} from "../../constants.ts";
|
|
32
|
+
import { registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
33
|
+
import type { ModelsDevProvider } from "../../lib/types.ts";
|
|
34
|
+
import { fetchWithRetry, isUsableModel } from "../../lib/util.ts";
|
|
35
|
+
import { createReRegister, enhanceWithCI } from "../../provider-helper.ts";
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Fetch + map
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
async function fetchNvidiaModels(
|
|
42
|
+
showPaid = false,
|
|
43
|
+
): Promise<ProviderModelConfig[]> {
|
|
44
|
+
const response = await fetchWithRetry(
|
|
45
|
+
URL_MODELS_DEV,
|
|
46
|
+
{
|
|
47
|
+
headers: { "User-Agent": "pi-free-providers" },
|
|
48
|
+
},
|
|
49
|
+
3,
|
|
50
|
+
1000,
|
|
51
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Failed to fetch models.dev: ${response.status} ${response.statusText}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const json = (await response.json()) as Record<string, ModelsDevProvider>;
|
|
61
|
+
const provider = Object.values(json).find((p) => p?.id === "nvidia");
|
|
62
|
+
if (!provider?.models)
|
|
63
|
+
throw new Error("nvidia provider not found in models.dev");
|
|
64
|
+
|
|
65
|
+
const result = applyHidden(
|
|
66
|
+
Object.values(provider.models)
|
|
67
|
+
.filter((m) => isUsableModel(m.id, NVIDIA_MIN_SIZE_B))
|
|
68
|
+
.filter((m) => {
|
|
69
|
+
// Filter non-chat models by modalities
|
|
70
|
+
const modalities = m.modalities;
|
|
71
|
+
if (modalities) {
|
|
72
|
+
const output = modalities.output ?? [];
|
|
73
|
+
const input = modalities.input ?? [];
|
|
74
|
+
if (!output.includes("text")) return false;
|
|
75
|
+
if (!input.includes("text")) return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
})
|
|
79
|
+
.filter((m) => {
|
|
80
|
+
// Filter by cost - free models have input cost of 0
|
|
81
|
+
if (!showPaid && (m.cost?.input ?? 0) > 0) return false;
|
|
82
|
+
return true;
|
|
83
|
+
})
|
|
84
|
+
.map(
|
|
85
|
+
(m): ProviderModelConfig => ({
|
|
86
|
+
id: m.id,
|
|
87
|
+
name: m.name,
|
|
88
|
+
reasoning: m.reasoning,
|
|
89
|
+
input: m.modalities?.input?.includes("image")
|
|
90
|
+
? ["text", "image"]
|
|
91
|
+
: ["text"],
|
|
92
|
+
cost: {
|
|
93
|
+
input: m.cost?.input ?? 0,
|
|
94
|
+
output: m.cost?.output ?? 0,
|
|
95
|
+
cacheRead: m.cost?.cache_read ?? 0,
|
|
96
|
+
cacheWrite: m.cost?.cache_write ?? 0,
|
|
97
|
+
},
|
|
98
|
+
contextWindow: m.limit.context,
|
|
99
|
+
maxTokens: m.limit.output,
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Extension Entry Point
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
export default async function (pi: ExtensionAPI) {
|
|
112
|
+
// Fetch both free and all models
|
|
113
|
+
let freeModels: ProviderModelConfig[] = [];
|
|
114
|
+
let allModels: ProviderModelConfig[] = [];
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
freeModels = await fetchNvidiaModels(false);
|
|
118
|
+
allModels = await fetchNvidiaModels(true);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error("[nvidia] Failed to fetch models at startup", error);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Store both sets for global toggle
|
|
125
|
+
const stored = { free: freeModels, all: allModels };
|
|
126
|
+
const apiKey = getNvidiaApiKey();
|
|
127
|
+
const hasKey = !!(apiKey || process.env.NVIDIA_API_KEY);
|
|
128
|
+
|
|
129
|
+
// Create re-register function
|
|
130
|
+
const reRegister = createReRegister(pi, {
|
|
131
|
+
providerId: PROVIDER_NVIDIA,
|
|
132
|
+
baseUrl: BASE_URL_NVIDIA,
|
|
133
|
+
apiKey: apiKey || "NVIDIA_API_KEY",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Register with global toggle system
|
|
137
|
+
registerWithGlobalToggle(PROVIDER_NVIDIA, stored, reRegister, hasKey);
|
|
138
|
+
|
|
139
|
+
// Register initial models (global toggle will apply filter if needed)
|
|
140
|
+
const initialModels = getNvidiaShowPaid() ? allModels : freeModels;
|
|
141
|
+
pi.registerProvider(PROVIDER_NVIDIA, {
|
|
142
|
+
baseUrl: BASE_URL_NVIDIA,
|
|
143
|
+
apiKey: apiKey || "NVIDIA_API_KEY",
|
|
144
|
+
api: "openai-completions" as const,
|
|
145
|
+
headers: {
|
|
146
|
+
"User-Agent": "pi-free-providers",
|
|
147
|
+
},
|
|
148
|
+
models: enhanceWithCI(initialModels),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Registration complete - models registered silently (use LOG_LEVEL=info to see details)
|
|
152
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama Cloud Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides access to Ollama's cloud-hosted models via ollama.com API.
|
|
5
|
+
* All models use Ollama's usage-based pricing system:
|
|
6
|
+
* - Free tier: Unlimited public models (session limits reset every 5 hours,
|
|
7
|
+
* weekly limits reset every 7 days)
|
|
8
|
+
* - Pro tier: 50x more cloud usage than Free
|
|
9
|
+
* - Max tier: 5x more usage than Pro
|
|
10
|
+
*
|
|
11
|
+
* Requires OLLAMA_API_KEY with cloud access.
|
|
12
|
+
* Get a free key at: https://ollama.com/settings/keys
|
|
13
|
+
*
|
|
14
|
+
* Responds to global free-only filter (shows models but warns they're freemium).
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* pi install git:github.com/apmantza/pi-free
|
|
18
|
+
* # Set OLLAMA_API_KEY env var
|
|
19
|
+
* # Models appear in /model selector
|
|
20
|
+
* # Use /toggle-ollama to show all vs limited set
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
ExtensionAPI,
|
|
25
|
+
ProviderModelConfig,
|
|
26
|
+
} from "@mariozechner/pi-coding-agent";
|
|
27
|
+
import {
|
|
28
|
+
applyHidden,
|
|
29
|
+
getOllamaApiKey,
|
|
30
|
+
getOllamaShowPaid,
|
|
31
|
+
} from "../../config.ts";
|
|
32
|
+
import {
|
|
33
|
+
BASE_URL_OLLAMA,
|
|
34
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
35
|
+
PROVIDER_OLLAMA,
|
|
36
|
+
} from "../../constants.ts";
|
|
37
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
38
|
+
import { registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
39
|
+
import { fetchWithRetry } from "../../lib/util.ts";
|
|
40
|
+
import { createReRegister, enhanceWithCI } from "../../provider-helper.ts";
|
|
41
|
+
|
|
42
|
+
const _logger = createLogger("ollama-cloud");
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Fetch + map
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
async function fetchOllamaModels(
|
|
49
|
+
apiKey: string,
|
|
50
|
+
): Promise<ProviderModelConfig[]> {
|
|
51
|
+
// Use OpenAI-compatible /v1/models endpoint for consistency
|
|
52
|
+
// The native /api/tags returns :cloud suffixes that may not work with /v1/chat/completions
|
|
53
|
+
const response = await fetchWithRetry(
|
|
54
|
+
`${BASE_URL_OLLAMA}/models`,
|
|
55
|
+
{
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${apiKey}`,
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
3,
|
|
62
|
+
1000,
|
|
63
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Failed to fetch Ollama models: ${response.status} ${response.statusText}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const json = (await response.json()) as {
|
|
73
|
+
data?: Array<{ id: string; owned_by?: string }>;
|
|
74
|
+
};
|
|
75
|
+
const models = json.data ?? [];
|
|
76
|
+
|
|
77
|
+
_logger.info(
|
|
78
|
+
`[ollama-cloud] Fetched ${models.length} models from Ollama Cloud`,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Filter to chat/text generation models only
|
|
82
|
+
const chatModels = models.filter((m) => {
|
|
83
|
+
// Skip embedding-only models (typically have "embed" in name)
|
|
84
|
+
const name = m.id.toLowerCase();
|
|
85
|
+
if (name.includes("embed")) return false;
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = applyHidden(
|
|
90
|
+
chatModels.map(
|
|
91
|
+
(m): ProviderModelConfig => ({
|
|
92
|
+
id: m.id,
|
|
93
|
+
name: m.id,
|
|
94
|
+
// Try to infer reasoning from model name
|
|
95
|
+
reasoning:
|
|
96
|
+
m.id.toLowerCase().includes("reasoning") ||
|
|
97
|
+
m.id.toLowerCase().includes("r1") ||
|
|
98
|
+
m.id.toLowerCase().includes("thinking"),
|
|
99
|
+
input: ["text"],
|
|
100
|
+
// Ollama Cloud uses usage-based pricing (GPU time), not per-token
|
|
101
|
+
// Free tier has limits but no direct cost per token
|
|
102
|
+
cost: {
|
|
103
|
+
input: 0, // Freemium: usage-based, not per-token
|
|
104
|
+
output: 0,
|
|
105
|
+
cacheRead: 0,
|
|
106
|
+
cacheWrite: 0,
|
|
107
|
+
},
|
|
108
|
+
// Default context window - Ollama doesn't expose this via /v1/models
|
|
109
|
+
contextWindow: 32768,
|
|
110
|
+
maxTokens: 4096, // Default, varies by model
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// Extension Entry Point
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
export default async function (pi: ExtensionAPI) {
|
|
123
|
+
const apiKey = getOllamaApiKey();
|
|
124
|
+
|
|
125
|
+
if (!apiKey) {
|
|
126
|
+
_logger.info(
|
|
127
|
+
"[ollama-cloud] Skipping - OLLAMA_API_KEY not set (env var or ~/.pi/free.json)",
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fetch models
|
|
133
|
+
let allModels: ProviderModelConfig[] = [];
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
allModels = await fetchOllamaModels(apiKey);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
_logger.error("[ollama-cloud] Failed to fetch models at startup", {
|
|
139
|
+
error: error instanceof Error ? error.message : String(error),
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// For Ollama, all models share the same free tier
|
|
145
|
+
// So "free" and "all" are the same set
|
|
146
|
+
const freeModels = allModels;
|
|
147
|
+
const stored = { free: freeModels, all: allModels };
|
|
148
|
+
const hasKey = true;
|
|
149
|
+
|
|
150
|
+
// Create re-register function
|
|
151
|
+
const reRegister = createReRegister(pi, {
|
|
152
|
+
providerId: PROVIDER_OLLAMA,
|
|
153
|
+
baseUrl: BASE_URL_OLLAMA,
|
|
154
|
+
apiKey,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Register with global toggle system
|
|
158
|
+
registerWithGlobalToggle(PROVIDER_OLLAMA, stored, reRegister, hasKey);
|
|
159
|
+
|
|
160
|
+
// Register initial models
|
|
161
|
+
const initialModels = getOllamaShowPaid() ? allModels : freeModels;
|
|
162
|
+
pi.registerProvider(PROVIDER_OLLAMA, {
|
|
163
|
+
baseUrl: BASE_URL_OLLAMA,
|
|
164
|
+
apiKey,
|
|
165
|
+
api: "openai-completions" as const,
|
|
166
|
+
models: enhanceWithCI(initialModels),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
_logger.info(
|
|
170
|
+
`[ollama-cloud] Registered ${initialModels.length} models (usage-based free tier)`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared OpenCode session/request tracking.
|
|
3
|
-
*
|
|
4
|
-
* OpenCode endpoints appear to behave more reliably when a stable session id
|
|
5
|
-
* is included across requests in the same Pi session.
|
|
6
|
-
*/
|
|
7
|
-
export function createOpenCodeSessionTracker() {
|
|
8
|
-
let sessionId = "";
|
|
9
|
-
let requestCount = 0;
|
|
10
|
-
|
|
11
|
-
function generateId(): string {
|
|
12
|
-
return (
|
|
13
|
-
Math.random().toString(36).substring(2, 15) +
|
|
14
|
-
Math.random().toString(36).substring(2, 15)
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getSessionId(): string {
|
|
19
|
-
if (!sessionId) {
|
|
20
|
-
sessionId = generateId();
|
|
21
|
-
}
|
|
22
|
-
return sessionId;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function nextRequestId(): string {
|
|
26
|
-
requestCount++;
|
|
27
|
-
return `${getSessionId()}-${requestCount}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
getSessionId,
|
|
32
|
-
nextRequestId,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared OpenCode session/request tracking.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode endpoints appear to behave more reliably when a stable session id
|
|
5
|
+
* is included across requests in the same Pi session.
|
|
6
|
+
*/
|
|
7
|
+
export function createOpenCodeSessionTracker() {
|
|
8
|
+
let sessionId = "";
|
|
9
|
+
let requestCount = 0;
|
|
10
|
+
|
|
11
|
+
function generateId(): string {
|
|
12
|
+
return (
|
|
13
|
+
Math.random().toString(36).substring(2, 15) +
|
|
14
|
+
Math.random().toString(36).substring(2, 15)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getSessionId(): string {
|
|
19
|
+
if (!sessionId) {
|
|
20
|
+
sessionId = generateId();
|
|
21
|
+
}
|
|
22
|
+
return sessionId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nextRequestId(): string {
|
|
26
|
+
requestCount++;
|
|
27
|
+
return `${getSessionId()}-${requestCount}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
getSessionId,
|
|
32
|
+
nextRequestId,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import crypto from "node:crypto";
|
|
16
|
-
import { spawn } from "node:child_process";
|
|
17
16
|
import type {
|
|
18
17
|
OAuthCredentials,
|
|
19
18
|
OAuthLoginCallbacks,
|
|
20
19
|
} from "@mariozechner/pi-ai";
|
|
21
|
-
import { createLogger } from "
|
|
20
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
21
|
+
import { openBrowser } from "../../lib/open-browser.ts";
|
|
22
22
|
|
|
23
23
|
const _logger = createLogger("qwen-auth");
|
|
24
24
|
|
|
@@ -37,6 +37,12 @@ const QWEN_OAUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
|
37
37
|
const INITIAL_POLL_INTERVAL_MS = 2000;
|
|
38
38
|
const MAX_POLL_INTERVAL_MS = 10000;
|
|
39
39
|
|
|
40
|
+
// Token refresh buffer: proactively refresh this many ms before actual expiry.
|
|
41
|
+
// Matches qwen-code's SharedTokenManager which uses a 30s buffer.
|
|
42
|
+
// We use 5 minutes (same as pi-core's reference qwen-cli example) to be safe
|
|
43
|
+
// against clock skew, network latency, and server-side early revocation.
|
|
44
|
+
const EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
45
|
+
|
|
40
46
|
// =============================================================================
|
|
41
47
|
// PKCE Utilities
|
|
42
48
|
// =============================================================================
|
|
@@ -46,10 +52,7 @@ function generateCodeVerifier(): string {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
function generateCodeChallenge(codeVerifier: string): string {
|
|
49
|
-
return crypto
|
|
50
|
-
.createHash("sha256")
|
|
51
|
-
.update(codeVerifier)
|
|
52
|
-
.digest("base64url");
|
|
55
|
+
return crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
function generatePKCEPair(): {
|
|
@@ -67,33 +70,10 @@ function generatePKCEPair(): {
|
|
|
67
70
|
|
|
68
71
|
function objectToUrlEncoded(data: Record<string, string>): string {
|
|
69
72
|
return Object.keys(data)
|
|
70
|
-
.map(
|
|
71
|
-
(key) =>
|
|
72
|
-
`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`,
|
|
73
|
-
)
|
|
73
|
+
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
|
|
74
74
|
.join("&");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function openBrowser(url: string): void {
|
|
78
|
-
try {
|
|
79
|
-
if (process.platform === "win32") {
|
|
80
|
-
// cmd.exe interprets & as a command separator, breaking URLs with query params.
|
|
81
|
-
// PowerShell's Start-Process treats the URL as a literal string.
|
|
82
|
-
spawn(
|
|
83
|
-
"powershell.exe",
|
|
84
|
-
["-NoProfile", "-NonInteractive", "-Command", `Start-Process "${url.replace(/"/g, '\\"')}"`],
|
|
85
|
-
{ detached: true, shell: false, windowsHide: true },
|
|
86
|
-
).unref();
|
|
87
|
-
} else if (process.platform === "darwin") {
|
|
88
|
-
spawn("open", [url], { detached: true }).unref();
|
|
89
|
-
} else {
|
|
90
|
-
spawn("xdg-open", [url], { detached: true }).unref();
|
|
91
|
-
}
|
|
92
|
-
} catch (err) {
|
|
93
|
-
_logger.debug("Failed to open browser", { error: String(err) });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
77
|
function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
98
78
|
return new Promise((resolve, reject) => {
|
|
99
79
|
if (signal?.aborted) {
|
|
@@ -169,9 +149,7 @@ async function requestDeviceAuthorization(
|
|
|
169
149
|
);
|
|
170
150
|
}
|
|
171
151
|
|
|
172
|
-
const result = (await response.json()) as
|
|
173
|
-
| DeviceAuthorizationData
|
|
174
|
-
| ErrorData;
|
|
152
|
+
const result = (await response.json()) as DeviceAuthorizationData | ErrorData;
|
|
175
153
|
|
|
176
154
|
if ("error" in result) {
|
|
177
155
|
throw new Error(
|
|
@@ -325,8 +303,8 @@ export async function loginQwen(
|
|
|
325
303
|
access: data.access_token!,
|
|
326
304
|
refresh: data.refresh_token ?? "",
|
|
327
305
|
expires: data.expires_in
|
|
328
|
-
? Date.now() + data.expires_in * 1000
|
|
329
|
-
: Date.now() + 3600 * 1000, // 1 hour default
|
|
306
|
+
? Date.now() + data.expires_in * 1000 - EXPIRY_BUFFER_MS
|
|
307
|
+
: Date.now() + 3600 * 1000 - EXPIRY_BUFFER_MS, // 1 hour default minus buffer
|
|
330
308
|
resource_url: resourceUrl,
|
|
331
309
|
};
|
|
332
310
|
}
|
|
@@ -354,7 +332,11 @@ export async function loginQwen(
|
|
|
354
332
|
export async function refreshQwenToken(
|
|
355
333
|
credentials: OAuthCredentials,
|
|
356
334
|
): Promise<OAuthCredentials> {
|
|
357
|
-
|
|
335
|
+
// Note: we intentionally DO NOT early-return when the token appears valid.
|
|
336
|
+
// pi-core calls refreshToken() only when it has already determined the token
|
|
337
|
+
// needs refreshing (Date.now() >= cred.expires). The early return was
|
|
338
|
+
// redundant and blocked forced-refreshes after server-side token revocation
|
|
339
|
+
// (where the stored expiry hasn't been reached yet but the token is invalid).
|
|
358
340
|
|
|
359
341
|
if (!credentials.refresh) {
|
|
360
342
|
throw new Error(
|
|
@@ -398,21 +380,23 @@ export async function refreshQwenToken(
|
|
|
398
380
|
}
|
|
399
381
|
|
|
400
382
|
// Preserve resource_url as a proper field (not encoded in refresh token)
|
|
401
|
-
const resourceUrl =
|
|
383
|
+
const resourceUrl =
|
|
384
|
+
data.resource_url || (credentials.resource_url as string) || "";
|
|
402
385
|
|
|
403
386
|
return {
|
|
404
387
|
access: data.access_token,
|
|
405
388
|
refresh: data.refresh_token ?? credentials.refresh,
|
|
406
389
|
expires: data.expires_in
|
|
407
|
-
? Date.now() + data.expires_in * 1000
|
|
408
|
-
: Date.now() + 3600 * 1000,
|
|
390
|
+
? Date.now() + data.expires_in * 1000 - EXPIRY_BUFFER_MS
|
|
391
|
+
: Date.now() + 3600 * 1000 - EXPIRY_BUFFER_MS,
|
|
409
392
|
resource_url: resourceUrl,
|
|
410
393
|
};
|
|
411
394
|
}
|
|
412
395
|
|
|
413
396
|
// Fallback endpoint used when resource_url is absent from the OAuth token.
|
|
414
397
|
// Mirrors qwen-code's DEFAULT_QWEN_BASE_URL.
|
|
415
|
-
const QWEN_DEFAULT_BASE_URL =
|
|
398
|
+
const QWEN_DEFAULT_BASE_URL =
|
|
399
|
+
"https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
416
400
|
|
|
417
401
|
/**
|
|
418
402
|
* Resolve the API base URL from OAuth credentials.
|