kiwimatecoder 0.1.0__py3-none-any.whl
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.
- kiwimatecoder/__init__.py +8 -0
- kiwimatecoder/__main__.py +12 -0
- kiwimatecoder/acp/__init__.py +13 -0
- kiwimatecoder/acp/protocol.py +115 -0
- kiwimatecoder/acp/server.py +823 -0
- kiwimatecoder/acp/stdio.py +32 -0
- kiwimatecoder/agent.py +1162 -0
- kiwimatecoder/ai.py +68 -0
- kiwimatecoder/audit.py +65 -0
- kiwimatecoder/browser.py +261 -0
- kiwimatecoder/catalog.py +445 -0
- kiwimatecoder/checkpoints.py +103 -0
- kiwimatecoder/client.py +820 -0
- kiwimatecoder/clipboard.py +99 -0
- kiwimatecoder/commands.py +6757 -0
- kiwimatecoder/config.py +5335 -0
- kiwimatecoder/diagnostics.py +295 -0
- kiwimatecoder/documents.py +299 -0
- kiwimatecoder/evals/__init__.py +39 -0
- kiwimatecoder/evals/cases.py +230 -0
- kiwimatecoder/evals/render.py +46 -0
- kiwimatecoder/evals/runner.py +319 -0
- kiwimatecoder/events.py +95 -0
- kiwimatecoder/forge.py +172 -0
- kiwimatecoder/git.py +164 -0
- kiwimatecoder/headless.py +255 -0
- kiwimatecoder/hooks.py +170 -0
- kiwimatecoder/hunks.py +260 -0
- kiwimatecoder/i18n.py +368 -0
- kiwimatecoder/images.py +195 -0
- kiwimatecoder/index/__init__.py +14 -0
- kiwimatecoder/index/builder.py +376 -0
- kiwimatecoder/index/embeddings.py +151 -0
- kiwimatecoder/index/search.py +247 -0
- kiwimatecoder/index/store.py +251 -0
- kiwimatecoder/instructions.py +83 -0
- kiwimatecoder/jobs.py +594 -0
- kiwimatecoder/lsp/__init__.py +56 -0
- kiwimatecoder/lsp/client.py +548 -0
- kiwimatecoder/lsp/manager.py +353 -0
- kiwimatecoder/lsp/protocol.py +79 -0
- kiwimatecoder/main.py +4258 -0
- kiwimatecoder/mcp/__init__.py +45 -0
- kiwimatecoder/mcp/client.py +385 -0
- kiwimatecoder/mcp/errors.py +7 -0
- kiwimatecoder/mcp/manager.py +253 -0
- kiwimatecoder/mcp/protocol.py +132 -0
- kiwimatecoder/media.py +904 -0
- kiwimatecoder/memory.py +141 -0
- kiwimatecoder/network.py +112 -0
- kiwimatecoder/notify.py +139 -0
- kiwimatecoder/permissions.py +171 -0
- kiwimatecoder/pickers.py +417 -0
- kiwimatecoder/plugins.py +245 -0
- kiwimatecoder/pricing.py +167 -0
- kiwimatecoder/prompts.py +213 -0
- kiwimatecoder/providers.py +342 -0
- kiwimatecoder/redaction.py +66 -0
- kiwimatecoder/remote.py +294 -0
- kiwimatecoder/repl.py +1388 -0
- kiwimatecoder/routing.py +65 -0
- kiwimatecoder/sandbox.py +169 -0
- kiwimatecoder/sdk.py +146 -0
- kiwimatecoder/session.py +574 -0
- kiwimatecoder/share.py +386 -0
- kiwimatecoder/shell.py +660 -0
- kiwimatecoder/skills.py +138 -0
- kiwimatecoder/sync.py +513 -0
- kiwimatecoder/team.py +249 -0
- kiwimatecoder/telemetry.py +392 -0
- kiwimatecoder/templates.py +108 -0
- kiwimatecoder/tools/__init__.py +190 -0
- kiwimatecoder/tools/ask.py +62 -0
- kiwimatecoder/tools/base.py +69 -0
- kiwimatecoder/tools/browser.py +261 -0
- kiwimatecoder/tools/edit_file.py +143 -0
- kiwimatecoder/tools/forge.py +207 -0
- kiwimatecoder/tools/git.py +158 -0
- kiwimatecoder/tools/image.py +300 -0
- kiwimatecoder/tools/io.py +346 -0
- kiwimatecoder/tools/list_dir.py +67 -0
- kiwimatecoder/tools/lsp.py +254 -0
- kiwimatecoder/tools/memory.py +115 -0
- kiwimatecoder/tools/paths.py +234 -0
- kiwimatecoder/tools/read_file.py +113 -0
- kiwimatecoder/tools/registry.py +91 -0
- kiwimatecoder/tools/run_bash.py +189 -0
- kiwimatecoder/tools/search.py +224 -0
- kiwimatecoder/tools/selection.py +108 -0
- kiwimatecoder/tools/shell.py +158 -0
- kiwimatecoder/tools/skill.py +46 -0
- kiwimatecoder/tools/task.py +61 -0
- kiwimatecoder/tools/todo.py +77 -0
- kiwimatecoder/tools/web.py +146 -0
- kiwimatecoder/tools/write_file.py +83 -0
- kiwimatecoder/ui.py +189 -0
- kiwimatecoder/updater.py +502 -0
- kiwimatecoder/web.py +499 -0
- kiwimatecoder-0.1.0.dist-info/METADATA +2016 -0
- kiwimatecoder-0.1.0.dist-info/RECORD +104 -0
- kiwimatecoder-0.1.0.dist-info/WHEEL +5 -0
- kiwimatecoder-0.1.0.dist-info/entry_points.txt +2 -0
- kiwimatecoder-0.1.0.dist-info/licenses/LICENSE +21 -0
- kiwimatecoder-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Built-in registry of model providers.
|
|
2
|
+
|
|
3
|
+
Most providers expose an OpenAI-compatible ``/chat/completions`` API so a single
|
|
4
|
+
:class:`~kiwimatecoder.client.UnifiedClient` can drive them. Providers whose
|
|
5
|
+
``compat`` is ``"anthropic"`` (Anthropic itself, or a custom provider configured
|
|
6
|
+
that way) are driven through the native Messages API with SSE streaming and
|
|
7
|
+
tool-use support instead.
|
|
8
|
+
|
|
9
|
+
There are no default models. The user chooses a model when they add a
|
|
10
|
+
provider (``kiwimatecoder setup``, ``/provider`` in the REPL, or
|
|
11
|
+
``config provider add``), and that choice is persisted per provider (see
|
|
12
|
+
``config.get_provider_model``). The ``models`` tuples below were verified in
|
|
13
|
+
July 2026 and are only suggestions for the pickers when the live listing is
|
|
14
|
+
unavailable: once a provider has a key, :mod:`kiwimatecoder.catalog` fetches
|
|
15
|
+
its live ``/models`` listing so newly released ids are offered and retired
|
|
16
|
+
ones disappear (see ``config.get_model_catalog``). Any id can still be set by
|
|
17
|
+
name with ``/model`` or ``config model set``, and the offered list can be
|
|
18
|
+
reshaped with ``/config models allow|deny``.
|
|
19
|
+
|
|
20
|
+
Local providers (Ollama, LM Studio, Unsloth) serve whatever models are loaded,
|
|
21
|
+
so when no model has been chosen the session model is read live from the
|
|
22
|
+
running server (see ``config.resolve_model``). Ollama and LM Studio need no API
|
|
23
|
+
key; Unsloth enforces auth even locally (``requires_key=True``), so its
|
|
24
|
+
``sk-unsloth-…`` key must be configured before the server can be used.
|
|
25
|
+
|
|
26
|
+
Experimental providers (``experimental=True``, currently KiwiMate) are listed
|
|
27
|
+
first and drawn in their own "Experimental" box by the provider pickers.
|
|
28
|
+
Providers with ``supports_tools=False`` are chat-only: the client sends them no
|
|
29
|
+
tool schemas and flattens any tool traffic in the history into plain text.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from urllib.parse import quote, urlparse
|
|
36
|
+
|
|
37
|
+
# Hosts that serve the local machine (LM Studio, Ollama, llama.cpp, Unsloth,
|
|
38
|
+
# ...). ``*.local`` hosts are treated the same way. Local does not imply
|
|
39
|
+
# keyless: see ``ProviderConfig.requires_key``.
|
|
40
|
+
_LOCAL_HOSTS = {"localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class ProviderConfig:
|
|
45
|
+
"""Static configuration for a single model provider."""
|
|
46
|
+
|
|
47
|
+
id: str
|
|
48
|
+
name: str
|
|
49
|
+
base_url: str # includes /v1, never a trailing /chat/completions
|
|
50
|
+
key_env: str
|
|
51
|
+
# Set for local servers that enforce auth anyway (Unsloth's sk-unsloth-…
|
|
52
|
+
# key). Keyless locals (Ollama, LM Studio) and custom providers leave it off.
|
|
53
|
+
requires_key: bool = False
|
|
54
|
+
compat: str = "openai" # "openai" | "anthropic" (native Messages API)
|
|
55
|
+
extra_headers: dict[str, str] = field(default_factory=dict)
|
|
56
|
+
# Suggested models offered by the pickers until a live listing is
|
|
57
|
+
# available; not exhaustive, and any id can still be set by name. None of
|
|
58
|
+
# them is a default: the user always chooses.
|
|
59
|
+
models: tuple[str, ...] = ()
|
|
60
|
+
# Auth header used for OpenAI-compatible requests. Cloud providers want
|
|
61
|
+
# ``Authorization: Bearer <key>``; Azure OpenAI wants ``api-key: <key>``.
|
|
62
|
+
# Both fields are ignored for native Anthropic providers (x-api-key).
|
|
63
|
+
key_header: str = "Authorization"
|
|
64
|
+
key_prefix: str = "Bearer "
|
|
65
|
+
# Azure-style API versioning: appended as ``?api-version=<value>`` to chat,
|
|
66
|
+
# catalog, and embedding URLs when non-empty.
|
|
67
|
+
api_version: str = ""
|
|
68
|
+
# Shown in its own "Experimental" box at the top of the provider pickers.
|
|
69
|
+
experimental: bool = False
|
|
70
|
+
# False for chat-only endpoints: no tool schemas are sent and tool calls /
|
|
71
|
+
# results already in the history are flattened into plain text.
|
|
72
|
+
supports_tools: bool = True
|
|
73
|
+
# Short note shown next to the provider in pickers and tables.
|
|
74
|
+
description: str = ""
|
|
75
|
+
# Per-message image limits enforced by the endpoint (0 = no limit). The
|
|
76
|
+
# client drops what would be rejected and says so in a text part.
|
|
77
|
+
max_images_per_message: int = 0
|
|
78
|
+
max_image_url_chars: int = 0
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def is_local(self) -> bool:
|
|
82
|
+
"""Whether the provider serves the local machine."""
|
|
83
|
+
host = (urlparse(self.base_url).hostname or "").lower()
|
|
84
|
+
return host in _LOCAL_HOSTS or host.endswith(".local")
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def needs_key(self) -> bool:
|
|
88
|
+
"""Whether requests fail without an API key (cloud or auth-enforcing local server)."""
|
|
89
|
+
return self.requires_key or not self.is_local
|
|
90
|
+
|
|
91
|
+
def versioned_url(self, url: str) -> str:
|
|
92
|
+
"""Append ``api_version`` as an ``api-version`` query when set.
|
|
93
|
+
|
|
94
|
+
Azure OpenAI versions its endpoint with a date query parameter, so the
|
|
95
|
+
client, catalog, and embeddings all build URLs through here. Providers
|
|
96
|
+
without an ``api_version`` get the URL back untouched.
|
|
97
|
+
"""
|
|
98
|
+
if not self.api_version:
|
|
99
|
+
return url
|
|
100
|
+
separator = "&" if "?" in url else "?"
|
|
101
|
+
return f"{url}{separator}api-version={quote(self.api_version, safe='')}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class UnknownProviderError(KeyError):
|
|
105
|
+
"""KeyError subclass for unknown provider IDs.
|
|
106
|
+
|
|
107
|
+
Subclassing preserves all existing ``except KeyError`` sites (in main,
|
|
108
|
+
commands, config, and tests). Overrides __str__ so f"{exc}" and the red
|
|
109
|
+
error prints produce clean messages without Python's extra repr quotes.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __str__(self) -> str:
|
|
113
|
+
return self.args[0] if self.args else super().__str__()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
REGISTRY: dict[str, ProviderConfig] = {
|
|
117
|
+
# Experimental: KiwiMate's own OpenAI-compatible API (kiwimate.net, keys
|
|
118
|
+
# start with sk-km-; see kiwimate.net/developers/docs). The endpoint is
|
|
119
|
+
# chat-only for now — it ignores tool schemas, rejects tool-role
|
|
120
|
+
# messages, drops system prompts, and caps replies at 1,024 tokens — so
|
|
121
|
+
# it runs with supports_tools=False and cannot edit files or run commands.
|
|
122
|
+
"kiwimate": ProviderConfig(
|
|
123
|
+
id="kiwimate",
|
|
124
|
+
name="KiwiMate",
|
|
125
|
+
base_url="https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions",
|
|
126
|
+
key_env="KIWIMATE_API_KEY",
|
|
127
|
+
models=(
|
|
128
|
+
"kiwimate-mini-1-0",
|
|
129
|
+
"kiwimate-small-1-0",
|
|
130
|
+
"kiwimate-medium-1-0",
|
|
131
|
+
"kiwimate-large-1-0",
|
|
132
|
+
),
|
|
133
|
+
experimental=True,
|
|
134
|
+
supports_tools=False,
|
|
135
|
+
description="kiwimate.net · chat only, no tool use yet",
|
|
136
|
+
# data: URLs only, at most 4 per message, each under 3,000,000 chars.
|
|
137
|
+
max_images_per_message=4,
|
|
138
|
+
max_image_url_chars=3_000_000,
|
|
139
|
+
),
|
|
140
|
+
"openai": ProviderConfig(
|
|
141
|
+
id="openai",
|
|
142
|
+
name="OpenAI",
|
|
143
|
+
base_url="https://api.openai.com/v1",
|
|
144
|
+
key_env="OPENAI_API_KEY",
|
|
145
|
+
models=("gpt-5.6-sol", "gpt-5.5"),
|
|
146
|
+
),
|
|
147
|
+
"anthropic": ProviderConfig(
|
|
148
|
+
id="anthropic",
|
|
149
|
+
name="Anthropic",
|
|
150
|
+
base_url="https://api.anthropic.com/v1",
|
|
151
|
+
key_env="ANTHROPIC_API_KEY",
|
|
152
|
+
compat="anthropic",
|
|
153
|
+
models=("claude-sonnet-5", "claude-opus-4-8", "claude-haiku-4-5"),
|
|
154
|
+
),
|
|
155
|
+
"google": ProviderConfig(
|
|
156
|
+
id="google",
|
|
157
|
+
name="Google Gemini",
|
|
158
|
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai",
|
|
159
|
+
key_env="GEMINI_API_KEY",
|
|
160
|
+
models=("gemini-3.5-flash", "gemini-3.5-pro"),
|
|
161
|
+
),
|
|
162
|
+
"xai": ProviderConfig(
|
|
163
|
+
id="xai",
|
|
164
|
+
name="xAI Grok",
|
|
165
|
+
base_url="https://api.x.ai/v1",
|
|
166
|
+
key_env="XAI_API_KEY",
|
|
167
|
+
models=("grok-4.5", "grok-build-0.1"),
|
|
168
|
+
),
|
|
169
|
+
"mistral": ProviderConfig(
|
|
170
|
+
id="mistral",
|
|
171
|
+
name="Mistral",
|
|
172
|
+
base_url="https://api.mistral.ai/v1",
|
|
173
|
+
key_env="MISTRAL_API_KEY",
|
|
174
|
+
models=("mistral-medium-3.5", "devstral-2512"),
|
|
175
|
+
),
|
|
176
|
+
"deepseek": ProviderConfig(
|
|
177
|
+
id="deepseek",
|
|
178
|
+
name="DeepSeek",
|
|
179
|
+
base_url="https://api.deepseek.com/v1",
|
|
180
|
+
key_env="DEEPSEEK_API_KEY",
|
|
181
|
+
models=("deepseek-v4-pro", "deepseek-chat", "deepseek-reasoner"),
|
|
182
|
+
),
|
|
183
|
+
"qwen": ProviderConfig(
|
|
184
|
+
id="qwen",
|
|
185
|
+
name="Qwen (Alibaba DashScope)",
|
|
186
|
+
base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
187
|
+
key_env="DASHSCOPE_API_KEY",
|
|
188
|
+
models=("qwen3.7-max", "qwen-plus", "qwen-turbo"),
|
|
189
|
+
),
|
|
190
|
+
"moonshot": ProviderConfig(
|
|
191
|
+
id="moonshot",
|
|
192
|
+
name="Moonshot (Kimi)",
|
|
193
|
+
base_url="https://api.moonshot.ai/v1",
|
|
194
|
+
key_env="MOONSHOT_API_KEY",
|
|
195
|
+
models=("kimi-k2.7-code", "kimi-latest"),
|
|
196
|
+
),
|
|
197
|
+
"openrouter": ProviderConfig(
|
|
198
|
+
id="openrouter",
|
|
199
|
+
name="OpenRouter",
|
|
200
|
+
base_url="https://openrouter.ai/api/v1",
|
|
201
|
+
key_env="OPENROUTER_API_KEY",
|
|
202
|
+
extra_headers={
|
|
203
|
+
"HTTP-Referer": "https://kiwimatecoder.com",
|
|
204
|
+
"X-Title": "KiwiMateCoder",
|
|
205
|
+
},
|
|
206
|
+
models=(
|
|
207
|
+
"anthropic/claude-sonnet-5",
|
|
208
|
+
"anthropic/claude-opus-4-8",
|
|
209
|
+
"openai/gpt-5.6-sol",
|
|
210
|
+
"google/gemini-3.5-flash",
|
|
211
|
+
"x-ai/grok-4.5",
|
|
212
|
+
"deepseek/deepseek-v4-pro",
|
|
213
|
+
"qwen/qwen3.7-max",
|
|
214
|
+
"moonshotai/kimi-k2.7-code",
|
|
215
|
+
"mistralai/devstral-2512",
|
|
216
|
+
"z-ai/glm-5.2",
|
|
217
|
+
),
|
|
218
|
+
),
|
|
219
|
+
"azure": ProviderConfig(
|
|
220
|
+
id="azure",
|
|
221
|
+
name="Azure OpenAI",
|
|
222
|
+
# Placeholder only: replace <resource> with your Azure resource name.
|
|
223
|
+
# Because built-in entries are not editable, the supported path is a
|
|
224
|
+
# custom provider pointed at your resource, e.g.
|
|
225
|
+
# ``config provider add my-azure "My Azure" \
|
|
226
|
+
# https://my-resource.openai.azure.com/openai/v1 my-deployment \
|
|
227
|
+
# --key-env AZURE_OPENAI_API_KEY --key-header api-key \
|
|
228
|
+
# --key-prefix "" --api-version 2024-10-21`` (the fourth argument is
|
|
229
|
+
# the model — your deployment name — to use with it).
|
|
230
|
+
base_url="https://<resource>.openai.azure.com/openai/v1",
|
|
231
|
+
key_env="AZURE_OPENAI_API_KEY",
|
|
232
|
+
key_header="api-key",
|
|
233
|
+
key_prefix="",
|
|
234
|
+
api_version="2024-10-21",
|
|
235
|
+
models=("gpt-5.6-sol", "gpt-5.5"),
|
|
236
|
+
),
|
|
237
|
+
"bedrock": ProviderConfig(
|
|
238
|
+
id="bedrock",
|
|
239
|
+
name="AWS Bedrock",
|
|
240
|
+
# Placeholder only: replace <region> with your AWS region. Bedrock's
|
|
241
|
+
# OpenAI-compatible runtime accepts a bearer token; SigV4/IAM signing is
|
|
242
|
+
# out of scope (use a signing proxy or custom provider if you need it).
|
|
243
|
+
base_url="https://bedrock-runtime.<region>.amazonaws.com/openai/v1",
|
|
244
|
+
key_env="AWS_BEARER_TOKEN_BEDROCK",
|
|
245
|
+
models=("openai.gpt-5.6-sol", "mistral.devstral-2512"),
|
|
246
|
+
),
|
|
247
|
+
"groq": ProviderConfig(
|
|
248
|
+
id="groq",
|
|
249
|
+
name="Groq",
|
|
250
|
+
base_url="https://api.groq.com/openai/v1",
|
|
251
|
+
key_env="GROQ_API_KEY",
|
|
252
|
+
models=("llama-4.1-70b-versatile", "qwen3.7-32b"),
|
|
253
|
+
),
|
|
254
|
+
"together": ProviderConfig(
|
|
255
|
+
id="together",
|
|
256
|
+
name="Together AI",
|
|
257
|
+
base_url="https://api.together.xyz/v1",
|
|
258
|
+
key_env="TOGETHER_API_KEY",
|
|
259
|
+
models=(
|
|
260
|
+
"meta-llama/Llama-4.1-70B-Instruct-Turbo",
|
|
261
|
+
"Qwen/Qwen3.7-72B-Instruct-Turbo",
|
|
262
|
+
),
|
|
263
|
+
),
|
|
264
|
+
"fireworks": ProviderConfig(
|
|
265
|
+
id="fireworks",
|
|
266
|
+
name="Fireworks AI",
|
|
267
|
+
base_url="https://api.fireworks.ai/inference/v1",
|
|
268
|
+
key_env="FIREWORKS_API_KEY",
|
|
269
|
+
models=(
|
|
270
|
+
"accounts/fireworks/models/llama-v4-70b-instruct",
|
|
271
|
+
"accounts/fireworks/models/qwen3.7-32b-instruct",
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
"cerebras": ProviderConfig(
|
|
275
|
+
id="cerebras",
|
|
276
|
+
name="Cerebras",
|
|
277
|
+
base_url="https://api.cerebras.ai/v1",
|
|
278
|
+
key_env="CEREBRAS_API_KEY",
|
|
279
|
+
models=("llama-4.1-70b", "qwen-3.7-32b"),
|
|
280
|
+
),
|
|
281
|
+
"deepinfra": ProviderConfig(
|
|
282
|
+
id="deepinfra",
|
|
283
|
+
name="DeepInfra",
|
|
284
|
+
base_url="https://api.deepinfra.com/v1/openai",
|
|
285
|
+
key_env="DEEPINFRA_API_KEY",
|
|
286
|
+
models=(
|
|
287
|
+
"meta-llama/Llama-4.1-70B-Instruct",
|
|
288
|
+
"Qwen/Qwen3.7-72B-Instruct",
|
|
289
|
+
),
|
|
290
|
+
),
|
|
291
|
+
"ollama": ProviderConfig(
|
|
292
|
+
id="ollama",
|
|
293
|
+
name="Ollama (local)",
|
|
294
|
+
base_url="http://localhost:11434/v1",
|
|
295
|
+
# With no chosen model, the session uses whatever the running server
|
|
296
|
+
# lists first. The suggested tuple only fills the offline picker.
|
|
297
|
+
key_env="OLLAMA_API_KEY", # optional; only if the server enforces auth
|
|
298
|
+
models=("llama3.1:8b", "qwen3:8b", "deepseek-r1:8b"),
|
|
299
|
+
),
|
|
300
|
+
"lmstudio": ProviderConfig(
|
|
301
|
+
id="lmstudio",
|
|
302
|
+
name="LM Studio (local)",
|
|
303
|
+
base_url="http://localhost:1234/v1",
|
|
304
|
+
key_env="LMSTUDIO_API_KEY", # optional; only if the server enforces auth
|
|
305
|
+
models=("qwen2.5-coder-7b-instruct", "llama-3.1-8b-instruct"),
|
|
306
|
+
),
|
|
307
|
+
"unsloth": ProviderConfig(
|
|
308
|
+
id="unsloth",
|
|
309
|
+
name="Unsloth (local)",
|
|
310
|
+
base_url="http://localhost:8888/v1",
|
|
311
|
+
# With no chosen model, the session uses whatever GGUF is loaded in
|
|
312
|
+
# Unsloth Studio. The suggested tuple only fills the offline picker.
|
|
313
|
+
# Required: Unsloth's local server enforces auth (Settings → API,
|
|
314
|
+
# the key starts with sk-unsloth-).
|
|
315
|
+
key_env="UNSLOTH_API_KEY",
|
|
316
|
+
requires_key=True,
|
|
317
|
+
models=("unsloth/Qwen3.6-27B-GGUF", "unsloth/gemma-4-26B-A4B-it-GGUF"),
|
|
318
|
+
),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
DEFAULT_PROVIDER_ID = "openrouter"
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def get_provider(provider_id: str) -> ProviderConfig:
|
|
325
|
+
"""Return the provider config for ``provider_id`` or raise ``UnknownProviderError`` (a ``KeyError`` subclass)."""
|
|
326
|
+
try:
|
|
327
|
+
return REGISTRY[provider_id]
|
|
328
|
+
except KeyError:
|
|
329
|
+
raise UnknownProviderError(
|
|
330
|
+
f"Unknown provider '{provider_id}'. "
|
|
331
|
+
+ f"Known providers: {', '.join(sorted(REGISTRY))}"
|
|
332
|
+
) from None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def list_providers() -> list[ProviderConfig]:
|
|
336
|
+
"""Return all registered providers in a stable order."""
|
|
337
|
+
return list(REGISTRY.values())
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def default_provider() -> ProviderConfig:
|
|
341
|
+
"""Return the default provider (OpenRouter, preserving legacy behavior)."""
|
|
342
|
+
return REGISTRY[DEFAULT_PROVIDER_ID]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Secret detection and redaction for logs and audit records.
|
|
2
|
+
|
|
3
|
+
Patterns cover common provider keys, cloud credentials, bearer tokens, and
|
|
4
|
+
``key = value`` assignments. Redaction is intentionally conservative: it is
|
|
5
|
+
applied to audit records, never to tool output that the model needs verbatim.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
PLACEHOLDER = "[REDACTED]"
|
|
14
|
+
|
|
15
|
+
_ASSIGNMENT = re.compile(
|
|
16
|
+
r"(?i)\b(api[_-]?key|access[_-]?key|secret|password|passwd|token)"
|
|
17
|
+
r"(\s*[:=]\s*)['\"]?([A-Za-z0-9._\-/+]{12,})"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
|
21
|
+
("anthropic_key", re.compile(r"\bsk-ant-[A-Za-z0-9_\-]{8,}")),
|
|
22
|
+
("openrouter_key", re.compile(r"\bsk-or-[A-Za-z0-9_\-]{8,}")),
|
|
23
|
+
("openai_key", re.compile(r"\bsk-[A-Za-z0-9_\-]{16,}")),
|
|
24
|
+
(
|
|
25
|
+
"github_token",
|
|
26
|
+
re.compile(
|
|
27
|
+
r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}"
|
|
28
|
+
r"|\bgithub_pat_[A-Za-z0-9_]{20,}"
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
("aws_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
|
|
32
|
+
("slack_token", re.compile(r"\bxox[baprs]-[A-Za-z0-9\-]{10,}")),
|
|
33
|
+
("bearer_token", re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._\-]{20,}")),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Finding:
|
|
39
|
+
"""One detected secret, with a short non-reversible preview."""
|
|
40
|
+
|
|
41
|
+
kind: str
|
|
42
|
+
preview: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def find_secrets(text: str) -> list[Finding]:
|
|
46
|
+
"""Return the secrets detected in ``text``."""
|
|
47
|
+
findings: list[Finding] = []
|
|
48
|
+
for kind, pattern in _PATTERNS:
|
|
49
|
+
for match in pattern.finditer(text):
|
|
50
|
+
value = match.group(0)
|
|
51
|
+
findings.append(Finding(kind=kind, preview=value[:4] + "…"))
|
|
52
|
+
for match in _ASSIGNMENT.finditer(text):
|
|
53
|
+
value = match.group(3)
|
|
54
|
+
findings.append(Finding(kind="assignment", preview=value[:4] + "…"))
|
|
55
|
+
return findings
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def redact(text: str, placeholder: str = PLACEHOLDER) -> str:
|
|
59
|
+
"""Replace detected secrets in ``text`` with ``placeholder``."""
|
|
60
|
+
result = text
|
|
61
|
+
for _kind, pattern in _PATTERNS:
|
|
62
|
+
result = pattern.sub(placeholder, result)
|
|
63
|
+
result = _ASSIGNMENT.sub(
|
|
64
|
+
lambda match: f"{match.group(1)}{match.group(2)}{placeholder}", result
|
|
65
|
+
)
|
|
66
|
+
return result
|