shinmentakezo07 commited on
Commit
8618eca
·
1 Parent(s): 9b71fd0

feat: add generic OpenAI-compatible endpoint provider

Browse files

Add an openai_compatible provider so users can point at any
OpenAI-compatible Chat Completions server with a custom base URL
(server root or /v1) and a comma-separated API key pool. The profile
sends no reasoning params and parses /think tags; keys rotate
round-robin with failover on transient errors like every other
provider. Admin manifest gains key/base URL/proxy fields, smoke
override, and helpful descriptions. Bump to 4.20.0.

.env.example CHANGED
@@ -11,6 +11,11 @@ AZURE_OPENAI_API_KEY=""
11
  AZURE_OPENAI_BASE_URL=""
12
 
13
 
 
 
 
 
 
14
  # OpenRouter Config
15
  OPENROUTER_API_KEY=""
16
 
@@ -125,7 +130,7 @@ OLLAMA_BASE_URL="http://localhost:11434"
125
 
126
  # All Claude model requests are mapped to these models, plain model is fallback
127
  # Format: provider_type/model/name
128
- # Valid providers: "nvidia_nim" | "openai" | "azure_openai" | "open_router" | "gemini" | "vertex" | "deepseek" | "mistral" | "mistral_codestral" | "opencode_zen" | "opencode_go" | "vercel" | "bedrock" | "huggingface" | "cohere" | "github_models" | "wafer" | "kimi" | "kimi_code" | "minimax" | "cerebras" | "groq" | "sambanova" | "kilo" | "fireworks" | "cloudflare" | "zai" | "ollama_cloud" | "lmstudio" | "llamacpp" | "ollama"
129
  MODEL_FABLE=
130
  MODEL_OPUS=
131
  MODEL_SONNET=
@@ -139,6 +144,7 @@ FCC_SMOKE_MODEL_NVIDIA_NIM=
139
  FCC_SMOKE_MODEL_OPENAI=
140
  FCC_SMOKE_MODEL_AZURE_OPENAI=
141
  FCC_SMOKE_MODEL_OPEN_ROUTER=
 
142
  FCC_SMOKE_MODEL_MISTRAL=
143
  FCC_SMOKE_MODEL_MISTRAL_REASONING=
144
  FCC_SMOKE_MODEL_MISTRAL_CODESTRAL=
@@ -188,6 +194,7 @@ REASONING_HAIKU=inherit
188
  # Per-provider proxy support: http and socks5, example: "http://username:password@host:port"
189
  OPENAI_PROXY=""
190
  AZURE_OPENAI_PROXY=""
 
191
  NVIDIA_NIM_PROXY=""
192
  OPENROUTER_PROXY=""
193
  MISTRAL_PROXY=""
 
11
  AZURE_OPENAI_BASE_URL=""
12
 
13
 
14
+ # Generic OpenAI-Compatible Endpoint (bring your own base URL; key accepts a comma-separated pool)
15
+ OPENAI_COMPATIBLE_API_KEY=""
16
+ OPENAI_COMPATIBLE_BASE_URL=""
17
+
18
+
19
  # OpenRouter Config
20
  OPENROUTER_API_KEY=""
21
 
 
130
 
131
  # All Claude model requests are mapped to these models, plain model is fallback
132
  # Format: provider_type/model/name
133
+ # Valid providers: "nvidia_nim" | "openai" | "azure_openai" | "openai_compatible" | "open_router" | "gemini" | "vertex" | "deepseek" | "mistral" | "mistral_codestral" | "opencode_zen" | "opencode_go" | "vercel" | "bedrock" | "huggingface" | "cohere" | "github_models" | "wafer" | "kimi" | "kimi_code" | "minimax" | "cerebras" | "groq" | "sambanova" | "kilo" | "fireworks" | "cloudflare" | "zai" | "ollama_cloud" | "lmstudio" | "llamacpp" | "ollama"
134
  MODEL_FABLE=
135
  MODEL_OPUS=
136
  MODEL_SONNET=
 
144
  FCC_SMOKE_MODEL_OPENAI=
145
  FCC_SMOKE_MODEL_AZURE_OPENAI=
146
  FCC_SMOKE_MODEL_OPEN_ROUTER=
147
+ FCC_SMOKE_MODEL_OPENAI_COMPATIBLE=
148
  FCC_SMOKE_MODEL_MISTRAL=
149
  FCC_SMOKE_MODEL_MISTRAL_REASONING=
150
  FCC_SMOKE_MODEL_MISTRAL_CODESTRAL=
 
194
  # Per-provider proxy support: http and socks5, example: "http://username:password@host:port"
195
  OPENAI_PROXY=""
196
  AZURE_OPENAI_PROXY=""
197
+ OPENAI_COMPATIBLE_PROXY=""
198
  NVIDIA_NIM_PROXY=""
199
  OPENROUTER_PROXY=""
200
  MISTRAL_PROXY=""
README.md CHANGED
@@ -155,6 +155,7 @@ fcc-codex exec "hello"
155
  | [NVIDIA NIM](https://build.nvidia.com/settings/api-keys) | `NVIDIA_NIM_API_KEY` | `nvidia_nim/nvidia/nemotron-3-super-120b-a12b` |
156
  | [OpenAI / ChatGPT](https://learn.chatgpt.com/docs/auth) | Connect ChatGPT in the Admin UI | `openai/<model-id>` |
157
  | [Azure OpenAI](https://learn.microsoft.com/azure/foundry/openai/how-to/chatgpt) | `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_BASE_URL` | `azure_openai/<deployment-name>` |
 
158
  | [OpenRouter](https://openrouter.ai/keys) | `OPENROUTER_API_KEY` | `open_router/openrouter/free` |
159
  | [Google AI Studio (Gemini)](https://aistudio.google.com/apikey) | `GEMINI_API_KEY` | `gemini/models/gemini-3.1-flash-lite` |
160
  | [Google Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/start/openai) | `VERTEX_PROJECT_ID` + ADC | `vertex/google/gemini-3.5-flash` |
@@ -197,6 +198,11 @@ fcc-codex exec "hello"
197
  `https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/`, and select a
198
  deployment that supports Chat Completions. Enter the deployment name as a
199
  custom model slug if it does not appear in the model dropdown.
 
 
 
 
 
200
  - Mistral Codestral uses a separate key from Mistral La Plateforme.
201
  - Kimi Code subscription keys use `kimi_code/`; Kimi API credit keys use
202
  `kimi/`. Kimi Code plans are for personal interactive coding-agent use under
 
155
  | [NVIDIA NIM](https://build.nvidia.com/settings/api-keys) | `NVIDIA_NIM_API_KEY` | `nvidia_nim/nvidia/nemotron-3-super-120b-a12b` |
156
  | [OpenAI / ChatGPT](https://learn.chatgpt.com/docs/auth) | Connect ChatGPT in the Admin UI | `openai/<model-id>` |
157
  | [Azure OpenAI](https://learn.microsoft.com/azure/foundry/openai/how-to/chatgpt) | `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_BASE_URL` | `azure_openai/<deployment-name>` |
158
+ | [OpenAI-Compatible Endpoint](https://platform.openai.com/docs/api-reference) | `OPENAI_COMPATIBLE_API_KEY` and `OPENAI_COMPATIBLE_BASE_URL` | `openai_compatible/<model-id>` |
159
  | [OpenRouter](https://openrouter.ai/keys) | `OPENROUTER_API_KEY` | `open_router/openrouter/free` |
160
  | [Google AI Studio (Gemini)](https://aistudio.google.com/apikey) | `GEMINI_API_KEY` | `gemini/models/gemini-3.1-flash-lite` |
161
  | [Google Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/start/openai) | `VERTEX_PROJECT_ID` + ADC | `vertex/google/gemini-3.5-flash` |
 
198
  `https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/`, and select a
199
  deployment that supports Chat Completions. Enter the deployment name as a
200
  custom model slug if it does not appear in the model dropdown.
201
+ - The generic OpenAI-Compatible Endpoint lets you point at your own Chat
202
+ Completions server. Set `OPENAI_COMPATIBLE_BASE_URL` to the server root
203
+ (for example `https://my-gateway.example`) or its `/v1` base, and
204
+ `OPENAI_COMPATIBLE_API_KEY` to a single key or a comma-separated key pool
205
+ that is rotated round-robin with failover like every other provider.
206
  - Mistral Codestral uses a separate key from Mistral La Plateforme.
207
  - Kimi Code subscription keys use `kimi_code/`; Kimi API credit keys use
208
  `kimi/`. Kimi Code plans are for personal interactive coding-agent use under
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
 
5
  [project]
6
  name = "free-claude-code"
7
- version = "4.19.0"
8
  description = "Local proxy connecting coding agents to OpenAI-compatible AI providers"
9
  readme = "README.md"
10
  requires-python = ">=3.14.0"
 
4
 
5
  [project]
6
  name = "free-claude-code"
7
+ version = "4.20.0"
8
  description = "Local proxy connecting coding agents to OpenAI-compatible AI providers"
9
  readme = "README.md"
10
  requires-python = ">=3.14.0"
src/free_claude_code/config/admin/manifest.py CHANGED
@@ -580,6 +580,12 @@ _NON_PROVIDER_FIELDS: tuple[ConfigFieldSpec, ...] = (
580
  "smoke",
581
  advanced=True,
582
  ),
 
 
 
 
 
 
583
  ConfigFieldSpec(
584
  "FCC_SMOKE_MODEL_MISTRAL",
585
  "Smoke Mistral Model",
 
580
  "smoke",
581
  advanced=True,
582
  ),
583
+ ConfigFieldSpec(
584
+ "FCC_SMOKE_MODEL_OPENAI_COMPATIBLE",
585
+ "Smoke OpenAI-Compatible Endpoint Model",
586
+ "smoke",
587
+ advanced=True,
588
+ ),
589
  ConfigFieldSpec(
590
  "FCC_SMOKE_MODEL_MISTRAL",
591
  "Smoke Mistral Model",
src/free_claude_code/config/admin/provider_manifest.py CHANGED
@@ -22,6 +22,18 @@ _PROVIDER_FIELD_OVERRIDES: dict[str, dict[str, Any]] = {
22
  "https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/."
23
  ),
24
  },
 
 
 
 
 
 
 
 
 
 
 
 
25
  "NVIDIA_NIM_API_KEY": {
26
  "label": "NVIDIA NIM API Key",
27
  "description": "Used by NVIDIA NIM chat and optional NIM voice transcription.",
 
22
  "https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/."
23
  ),
24
  },
25
+ "OPENAI_COMPATIBLE_API_KEY": {
26
+ "description": (
27
+ "API key for your OpenAI-compatible endpoint. Accepts a comma-separated "
28
+ "key pool that is rotated round-robin with failover on transient errors."
29
+ ),
30
+ },
31
+ "OPENAI_COMPATIBLE_BASE_URL": {
32
+ "description": (
33
+ "Base URL of your OpenAI-compatible Chat Completions server, for example "
34
+ "https://my-gateway.example or https://my-gateway.example/v1."
35
+ ),
36
+ },
37
  "NVIDIA_NIM_API_KEY": {
38
  "label": "NVIDIA NIM API Key",
39
  "description": "Used by NVIDIA NIM chat and optional NIM voice transcription.",
src/free_claude_code/config/provider_catalog.py CHANGED
@@ -116,6 +116,20 @@ PROVIDER_CATALOG: dict[str, ProviderDescriptor] = {
116
  "azure_openai_base_url",
117
  ),
118
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  "open_router": ProviderDescriptor(
120
  provider_id="open_router",
121
  display_name="OpenRouter",
 
116
  "azure_openai_base_url",
117
  ),
118
  ),
119
+ # Bring-your-own OpenAI-compatible Chat Completions endpoint. The customer
120
+ # owns the base URL; credentials accept a comma-separated key pool.
121
+ "openai_compatible": ProviderDescriptor(
122
+ provider_id="openai_compatible",
123
+ display_name="OpenAI-Compatible Endpoint",
124
+ credential_env="OPENAI_COMPATIBLE_API_KEY",
125
+ credential_attr="openai_compatible_api_key",
126
+ base_url_attr="openai_compatible_base_url",
127
+ proxy_attr="openai_compatible_proxy",
128
+ required_settings_attrs=(
129
+ "openai_compatible_api_key",
130
+ "openai_compatible_base_url",
131
+ ),
132
+ ),
133
  "open_router": ProviderDescriptor(
134
  provider_id="open_router",
135
  display_name="OpenRouter",
src/free_claude_code/config/settings.py CHANGED
@@ -28,6 +28,15 @@ class Settings(BaseSettings):
28
  default="", validation_alias="AZURE_OPENAI_BASE_URL"
29
  )
30
 
 
 
 
 
 
 
 
 
 
31
  # ==================== OpenRouter Config ====================
32
  open_router_api_key: str = Field(default="", validation_alias="OPENROUTER_API_KEY")
33
 
@@ -163,6 +172,9 @@ class Settings(BaseSettings):
163
  # ==================== Per-Provider Proxy ====================
164
  openai_proxy: str = Field(default="", validation_alias="OPENAI_PROXY")
165
  azure_openai_proxy: str = Field(default="", validation_alias="AZURE_OPENAI_PROXY")
 
 
 
166
  nvidia_nim_proxy: str = Field(default="", validation_alias="NVIDIA_NIM_PROXY")
167
  open_router_proxy: str = Field(default="", validation_alias="OPENROUTER_PROXY")
168
  mistral_proxy: str = Field(default="", validation_alias="MISTRAL_PROXY")
 
28
  default="", validation_alias="AZURE_OPENAI_BASE_URL"
29
  )
30
 
31
+ # ==================== Generic OpenAI-Compatible Endpoint ====================
32
+ # Bring-your-own base URL; the key accepts a comma-separated pool.
33
+ openai_compatible_api_key: str = Field(
34
+ default="", validation_alias="OPENAI_COMPATIBLE_API_KEY"
35
+ )
36
+ openai_compatible_base_url: str = Field(
37
+ default="", validation_alias="OPENAI_COMPATIBLE_BASE_URL"
38
+ )
39
+
40
  # ==================== OpenRouter Config ====================
41
  open_router_api_key: str = Field(default="", validation_alias="OPENROUTER_API_KEY")
42
 
 
172
  # ==================== Per-Provider Proxy ====================
173
  openai_proxy: str = Field(default="", validation_alias="OPENAI_PROXY")
174
  azure_openai_proxy: str = Field(default="", validation_alias="AZURE_OPENAI_PROXY")
175
+ openai_compatible_proxy: str = Field(
176
+ default="", validation_alias="OPENAI_COMPATIBLE_PROXY"
177
+ )
178
  nvidia_nim_proxy: str = Field(default="", validation_alias="NVIDIA_NIM_PROXY")
179
  open_router_proxy: str = Field(default="", validation_alias="OPENROUTER_PROXY")
180
  mistral_proxy: str = Field(default="", validation_alias="MISTRAL_PROXY")
src/free_claude_code/providers/openai_chat/profiles.py CHANGED
@@ -152,6 +152,20 @@ OPENAI_CHAT_PROFILES: dict[str, OpenAIChatProfile] = {
152
  ),
153
  model_ids_are_routable=False,
154
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  "mistral_codestral": OpenAIChatProfile(
156
  _policy("CODESTRAL", ReasoningReplayMode.THINK_TAGS),
157
  NO_REASONING,
 
152
  ),
153
  model_ids_are_routable=False,
154
  ),
155
+ # Generic bring-your-own OpenAI-compatible Chat Completions endpoint.
156
+ # Send no reasoning params (many self-hosted servers reject unknown fields);
157
+ # THINK_TAGS replay still surfaces /think delimiters models emit. Accepts a
158
+ # server root or an explicit /v1 base, and passes caller extra_body through.
159
+ "openai_compatible": OpenAIChatProfile(
160
+ _policy(
161
+ "OPENAI_COMPATIBLE",
162
+ ReasoningReplayMode.THINK_TAGS,
163
+ include_extra_body=True,
164
+ extra_body_validator=validate_extra_body_does_not_override_canonical_fields,
165
+ ),
166
+ NO_REASONING,
167
+ normalize_base_url=True,
168
+ ),
169
  "mistral_codestral": OpenAIChatProfile(
170
  _policy("CODESTRAL", ReasoningReplayMode.THINK_TAGS),
171
  NO_REASONING,
tests/contracts/test_admin_provider_manifest.py CHANGED
@@ -192,3 +192,39 @@ def test_azure_openai_admin_status_distinguishes_key_and_url() -> None:
192
  )["status"]
193
  == "configured"
194
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  )["status"]
193
  == "configured"
194
  )
195
+
196
+
197
+ def test_openai_compatible_admin_status_distinguishes_key_and_url() -> None:
198
+ from free_claude_code.config.admin.status import provider_config_status
199
+
200
+ def compatible_status(api_key: str, base_url: str) -> dict[str, object]:
201
+ statuses = provider_config_status(
202
+ {
203
+ "OPENAI_COMPATIBLE_API_KEY": {"value": api_key},
204
+ "OPENAI_COMPATIBLE_BASE_URL": {"value": base_url},
205
+ }
206
+ )
207
+ return next(
208
+ status
209
+ for status in statuses
210
+ if status["provider_id"] == "openai_compatible"
211
+ )
212
+
213
+ missing_key = compatible_status("", "https://my-gateway.example/v1")
214
+ assert missing_key["status"] == "missing_key"
215
+ assert missing_key["label"] == "Missing key"
216
+
217
+ missing_url = compatible_status("gateway-key", "")
218
+ assert missing_url["status"] == "missing_config"
219
+ assert missing_url["label"] == "Missing configuration"
220
+ assert missing_url["configuration"] == (
221
+ "OPENAI_COMPATIBLE_API_KEY + OPENAI_COMPATIBLE_BASE_URL"
222
+ )
223
+
224
+ assert (
225
+ compatible_status(
226
+ "gateway-key",
227
+ "https://my-gateway.example/v1",
228
+ )["status"]
229
+ == "configured"
230
+ )
tests/contracts/test_provider_catalog_order.py CHANGED
@@ -9,6 +9,7 @@ _EXPECTED_PROVIDER_ORDER: tuple[str, ...] = (
9
  "nvidia_nim",
10
  "openai",
11
  "azure_openai",
 
12
  "open_router",
13
  "gemini",
14
  "vertex",
 
9
  "nvidia_nim",
10
  "openai",
11
  "azure_openai",
12
+ "openai_compatible",
13
  "open_router",
14
  "gemini",
15
  "vertex",
tests/providers/test_provider_runtime.py CHANGED
@@ -55,6 +55,8 @@ def _make_settings(**overrides):
55
  mock.model_haiku = None
56
  mock.azure_openai_api_key = "test_azure_openai_key"
57
  mock.azure_openai_base_url = "https://test-resource.openai.azure.com/openai/v1/"
 
 
58
  mock.nvidia_nim_api_key = "test_key"
59
  mock.open_router_api_key = "test_openrouter_key"
60
  mock.mistral_api_key = "test_mistral_key"
@@ -113,6 +115,7 @@ def _make_settings(**overrides):
113
  mock.kilo_proxy = ""
114
  mock.openai_proxy = ""
115
  mock.azure_openai_proxy = ""
 
116
  mock.provider_rate_limit = 40
117
  mock.provider_rate_window = 60
118
  mock.provider_max_concurrency = 5
@@ -234,6 +237,63 @@ def test_azure_openai_provider_config_reports_missing_resource_url() -> None:
234
  )
235
 
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  @pytest.mark.parametrize(
238
  ("provider_id", "expected_api_key"),
239
  [
@@ -472,6 +532,7 @@ def test_create_provider_instantiates_each_builtin():
472
  "nvidia_nim": NvidiaNimProvider,
473
  "openai": OpenAICodexProvider,
474
  "azure_openai": OpenAIChatProvider,
 
475
  "open_router": OpenRouterProvider,
476
  "mistral": MistralProvider,
477
  "mistral_codestral": OpenAIChatProvider,
 
55
  mock.model_haiku = None
56
  mock.azure_openai_api_key = "test_azure_openai_key"
57
  mock.azure_openai_base_url = "https://test-resource.openai.azure.com/openai/v1/"
58
+ mock.openai_compatible_api_key = "test_openai_compatible_key"
59
+ mock.openai_compatible_base_url = "https://my-gateway.example/v1"
60
  mock.nvidia_nim_api_key = "test_key"
61
  mock.open_router_api_key = "test_openrouter_key"
62
  mock.mistral_api_key = "test_mistral_key"
 
115
  mock.kilo_proxy = ""
116
  mock.openai_proxy = ""
117
  mock.azure_openai_proxy = ""
118
+ mock.openai_compatible_proxy = ""
119
  mock.provider_rate_limit = 40
120
  mock.provider_rate_window = 60
121
  mock.provider_max_concurrency = 5
 
237
  )
238
 
239
 
240
+ def test_openai_compatible_provider_config_uses_custom_base_key_and_proxy() -> None:
241
+ descriptor = PROVIDER_CATALOG["openai_compatible"]
242
+ settings = _make_settings(
243
+ openai_compatible_api_key="gateway-1, gateway-2",
244
+ openai_compatible_base_url="https://my-gateway.example/v1",
245
+ openai_compatible_proxy="http://proxy.test:8080",
246
+ )
247
+
248
+ config = build_provider_config(descriptor, settings)
249
+
250
+ assert descriptor.default_base_url is None
251
+ assert descriptor.configuration_attrs() == (
252
+ "openai_compatible_api_key",
253
+ "openai_compatible_base_url",
254
+ )
255
+ assert config.api_key == "gateway-1"
256
+ assert config.api_keys == ("gateway-1", "gateway-2")
257
+ assert config.base_url == "https://my-gateway.example/v1"
258
+ assert config.proxy == "http://proxy.test:8080"
259
+
260
+
261
+ def test_openai_compatible_provider_config_reports_missing_base_url() -> None:
262
+ descriptor = PROVIDER_CATALOG["openai_compatible"]
263
+
264
+ with pytest.raises(
265
+ ApplicationUnavailableError,
266
+ match="OPENAI_COMPATIBLE_BASE_URL is not set",
267
+ ):
268
+ build_provider_config(
269
+ descriptor,
270
+ _make_settings(
271
+ openai_compatible_api_key="gateway-token",
272
+ openai_compatible_base_url="",
273
+ ),
274
+ )
275
+
276
+
277
+ def test_create_openai_compatible_provider_normalizes_base_url_and_pools_keys() -> None:
278
+ settings = _make_settings(
279
+ openai_compatible_api_key="k1, k2",
280
+ openai_compatible_base_url="https://my-gateway.example",
281
+ )
282
+
283
+ with patch(
284
+ "free_claude_code.providers.openai_chat.provider.AsyncOpenAI"
285
+ ) as client_cls:
286
+ provider = create_provider("openai_compatible", settings)
287
+
288
+ assert isinstance(provider, OpenAIChatProvider)
289
+ # Server root is normalized to the /v1 OpenAI-compatible base.
290
+ assert provider._base_url == "https://my-gateway.example/v1"
291
+ assert provider._provider_name == "OPENAI_COMPATIBLE"
292
+ assert provider._pool is not None
293
+ assert provider._pool.size == 2
294
+ assert client_cls.call_count == 2
295
+
296
+
297
  @pytest.mark.parametrize(
298
  ("provider_id", "expected_api_key"),
299
  [
 
532
  "nvidia_nim": NvidiaNimProvider,
533
  "openai": OpenAICodexProvider,
534
  "azure_openai": OpenAIChatProvider,
535
+ "openai_compatible": OpenAIChatProvider,
536
  "open_router": OpenRouterProvider,
537
  "mistral": MistralProvider,
538
  "mistral_codestral": OpenAIChatProvider,
uv.lock CHANGED
@@ -620,7 +620,7 @@ wheels = [
620
 
621
  [[package]]
622
  name = "free-claude-code"
623
- version = "4.19.0"
624
  source = { editable = "." }
625
  dependencies = [
626
  { name = "aiohttp" },
 
620
 
621
  [[package]]
622
  name = "free-claude-code"
623
+ version = "4.20.0"
624
  source = { editable = "." }
625
  dependencies = [
626
  { name = "aiohttp" },