pi-free 2.0.1 → 2.0.4

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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * CrofAI Provider Extension
3
+ *
4
+ * Provides access to CrofAI API - OpenAI-compatible LLM inference service.
5
+ *
6
+ * Setup:
7
+ * 1. Get API key from https://ai.nahcrof.com
8
+ * 2. Set CROFAI_API_KEY env var or add to ~/.pi/free.json
9
+ *
10
+ * Responds to global free-only filter.
11
+ *
12
+ * Usage:
13
+ * pi install git:github.com/apmantza/pi-free
14
+ * # Set CROFAI_API_KEY env var
15
+ * # Models appear in /model selector
16
+ */
17
+
18
+ import type {
19
+ ExtensionAPI,
20
+ ProviderModelConfig,
21
+ } from "@mariozechner/pi-coding-agent";
22
+ import { getCrofaiApiKey, getCrofaiShowPaid } from "../../config.ts";
23
+ import {
24
+ BASE_URL_CROFAI,
25
+ DEFAULT_FETCH_TIMEOUT_MS,
26
+ PROVIDER_CROFAI,
27
+ } from "../../constants.ts";
28
+ import { createLogger } from "../../lib/logger.ts";
29
+ import {
30
+ getProxyModelCompat,
31
+ isLikelyReasoningModel,
32
+ } from "../../lib/provider-compat.ts";
33
+ import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
34
+ import { fetchWithRetry } from "../../lib/util.ts";
35
+ import { createReRegister, setupProvider } from "../../provider-helper.ts";
36
+
37
+ const _logger = createLogger("crofai");
38
+
39
+ // =============================================================================
40
+ // Fetch CrofAI models
41
+ // =============================================================================
42
+
43
+ interface CrofaiModel {
44
+ id: string;
45
+ object?: string;
46
+ created?: number;
47
+ owned_by?: string;
48
+ }
49
+
50
+ async function fetchCrofaiModels(
51
+ apiKey: string,
52
+ ): Promise<ProviderModelConfig[]> {
53
+ _logger.info("[crofai] Fetching models from CrofAI API...");
54
+
55
+ try {
56
+ const response = await fetchWithRetry(
57
+ `${BASE_URL_CROFAI}/models`,
58
+ {
59
+ headers: {
60
+ Authorization: `Bearer ${apiKey}`,
61
+ "Content-Type": "application/json",
62
+ },
63
+ },
64
+ 3,
65
+ 1000,
66
+ DEFAULT_FETCH_TIMEOUT_MS,
67
+ );
68
+
69
+ if (!response.ok) {
70
+ throw new Error(`CrofAI API error: ${response.status}`);
71
+ }
72
+
73
+ const data = (await response.json()) as { data?: CrofaiModel[] };
74
+ const models = data.data ?? [];
75
+
76
+ _logger.info(`[crofai] Fetched ${models.length} models`);
77
+
78
+ return models
79
+ .filter((m) => m.id) // Filter out any empty entries
80
+ .map((m) => {
81
+ const name = m.id.split("/").pop() || m.id;
82
+ return {
83
+ id: m.id,
84
+ name,
85
+ reasoning: isLikelyReasoningModel({ id: m.id, name }),
86
+ input: ["text"],
87
+ cost: {
88
+ input: 0, // CrofAI doesn't expose pricing via API
89
+ output: 0,
90
+ cacheRead: 0,
91
+ cacheWrite: 0,
92
+ },
93
+ contextWindow: 128000, // Default, varies by model
94
+ maxTokens: 4096,
95
+ compat: getProxyModelCompat({ id: m.id, name }),
96
+ } satisfies ProviderModelConfig;
97
+ });
98
+ } catch (error) {
99
+ _logger.error("[crofai] Failed to fetch models:", {
100
+ error: error instanceof Error ? error.message : String(error),
101
+ });
102
+ return [];
103
+ }
104
+ }
105
+
106
+ // =============================================================================
107
+ // Extension Entry Point
108
+ // =============================================================================
109
+
110
+ export default async function crofaiProvider(pi: ExtensionAPI) {
111
+ const apiKey = getCrofaiApiKey();
112
+
113
+ if (!apiKey) {
114
+ _logger.info(
115
+ "[crofai] Skipping - CROFAI_API_KEY not set (env var or ~/.pi/free.json)",
116
+ );
117
+ return;
118
+ }
119
+
120
+ // Fetch models
121
+ const allModels = await fetchCrofaiModels(apiKey);
122
+
123
+ if (allModels.length === 0) {
124
+ _logger.warn("[crofai] No models available");
125
+ return;
126
+ }
127
+
128
+ // Use isFreeModel with allModels for proper detection
129
+ // CrofAI doesn't expose pricing (all costs are $0), so Route B will be used:
130
+ // FREE only if "free" in name
131
+ const freeModels = allModels.filter((m) =>
132
+ isFreeModel({ ...m, provider: PROVIDER_CROFAI }, allModels),
133
+ );
134
+
135
+ const stored = { free: freeModels, all: allModels };
136
+
137
+ _logger.info(
138
+ `[crofai] Registered ${allModels.length} models (${freeModels.length} free)`,
139
+ );
140
+
141
+ // Create re-register function
142
+ const reRegister = createReRegister(pi, {
143
+ providerId: PROVIDER_CROFAI,
144
+ baseUrl: BASE_URL_CROFAI,
145
+ apiKey,
146
+ });
147
+
148
+ // Register with global toggle
149
+ registerWithGlobalToggle(PROVIDER_CROFAI, stored, reRegister, true);
150
+
151
+ // Setup provider with toggle command
152
+ setupProvider(
153
+ pi,
154
+ {
155
+ providerId: PROVIDER_CROFAI,
156
+ initialShowPaid: getCrofaiShowPaid(),
157
+ reRegister: (models, _stored) => {
158
+ if (_stored) {
159
+ stored.free = _stored.free;
160
+ stored.all = _stored.all;
161
+ }
162
+ reRegister(models);
163
+ },
164
+ },
165
+ stored,
166
+ );
167
+
168
+ // Initial registration
169
+ reRegister(freeModels);
170
+ }