mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
Merge remote-tracking branch 'origin/main' into puddle-barometer
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show underlying models for every Auto model in the model selector when the catalog includes them.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Automatically expose broad reasoning effort options for custom provider models and link saved providers to advanced JSON configuration.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Disable the suggest tool and auto-dismiss pending suggestions in non-interactive CLI runs to prevent hanging on benchmarks and automated pipelines.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/kilo-memory": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Reduce noisy memory timeout warnings and retry transient background consolidation failures once.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@kilocode/kilo-gateway": patch
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Ignore negative pricing entries from model catalogs and handle unpriced models gracefully in UI price formatting.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix Mermaid Copy PNG/SVG in VS Code webviews so they put images on the clipboard instead of failing or copying SVG markup.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Set explicit prompt cache breakpoints on stable prefixes for OpenAI GPT-5.6+ models.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support browsing an instance's model catalog before a session starts.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show the permission approval reason for reads and writes outside the workspace, matching other tools
|
||||
@@ -64,6 +64,10 @@ export const CodeWithAiNav: NavSection[] = [
|
||||
href: "/code-with-ai/agents/chat-interface",
|
||||
children: "Chat Interface",
|
||||
},
|
||||
{
|
||||
href: "/code-with-ai/agents/session-history",
|
||||
children: "Session History and Search",
|
||||
},
|
||||
{
|
||||
href: "/code-with-ai/agents/context-mentions",
|
||||
children: "Context & Mentions",
|
||||
|
||||
@@ -63,7 +63,7 @@ Open your terminal and run `kilo` to launch the interactive terminal interface (
|
||||
- **Input prompt** - Type your requests and press Enter to send
|
||||
- **Action buttons** - Approve or reject proposed changes, answer questions
|
||||
- **Agent dropdown** - Switch between agents (e.g. Code, Ask, Plan) from the sidebar
|
||||
- **Session management** - Start new sessions or resume previous ones
|
||||
- **Session management** - Start new sessions or [search and resume previous ones](/docs/code-with-ai/agents/session-history)
|
||||
|
||||
**Providing context:**
|
||||
|
||||
@@ -89,7 +89,7 @@ You can also rename local sessions from **History** using the edit button or the
|
||||
- **Input prompt** - Type your requests and press Enter to send
|
||||
- **Action buttons** - Approve or reject proposed changes, answer questions
|
||||
- **Agent cycling** - Switch between agents using keybinds or slash commands
|
||||
- **Session management** - Start new sessions or resume previous ones
|
||||
- **Session management** - Start new sessions or [search and resume previous ones](/docs/code-with-ai/agents/session-history)
|
||||
- **New task** - Start a new task, available using the `+` button at the top or `New Task` button above the chat input
|
||||
- **Worktree** - Continue the current task with it's git state and session history in the Agent Manager in an isolated worktree
|
||||
- **File changes** - Shows the number of lines changed and opens a diff view
|
||||
|
||||
@@ -25,15 +25,15 @@ Type `@` in the chat input to get autocomplete suggestions. You can mention:
|
||||
| **File** | Attach a file's contents to your message | `@src/utils.ts` |
|
||||
| **Terminal** | Include your active VS Code terminal output | `@terminal` |
|
||||
| **Git Changes** | Attach uncommitted working-tree diffs and new files | `@git-changes` |
|
||||
| **Past chats** | Attach a previous session's transcript as context | `@` → **Past chats** → pick a session |
|
||||
| **Past chats** | Add a previous session's chat history as context | `@` → **Past chats** → pick a session |
|
||||
|
||||
Selecting a suggestion inserts the mention and highlights it in the input. File contents, terminal output, and git changes are attached as context when you send the message.
|
||||
|
||||
### Referencing Past Chats
|
||||
|
||||
Choosing **Past chats** from the `@` menu opens a searchable picker of your previous sessions in the current workspace, ordered by recency. Selecting a session inserts a highlighted mention token; when you send the message, that session's current transcript is attached as context so the agent can build on the earlier conversation. Clicking the mention token opens the referenced session.
|
||||
Choosing **Past chats** from the `@` menu opens a searchable picker of sessions in the current workspace and related Git worktrees, including Agent Manager worktrees, ordered by recency. Search matches session titles and worktree names. Selecting a session inserts a highlighted mention token; when you send the message, that session's current chat history is added as context so the agent can build on the earlier conversation. Clicking the mention token opens the referenced session.
|
||||
|
||||
Very long transcripts are truncated, keeping the beginning and end, so a single mention cannot fill the context window.
|
||||
Very long chat histories are truncated, keeping the beginning and end, so a single mention cannot fill the context window.
|
||||
|
||||
### Drag and Drop
|
||||
|
||||
@@ -77,7 +77,7 @@ This means the agent can explore your entire project as needed, rather than bein
|
||||
| **Mention files when helpful** | If you know the exact file, mention its path to save the agent a search step |
|
||||
| **Keep editor tabs relevant** | Open tabs are passed as context, so keep relevant files open |
|
||||
| **Trust the agent's tools** | The agent can search, read, and explore your codebase — let it do the discovery work |
|
||||
| **Reference a past chat** | Type `@` and choose **Past chats** to open a searchable picker of previous sessions in the current workspace. Selecting a session attaches its current transcript as context when you send the message. |
|
||||
| **Reference a past chat** | Type `@` and choose **Past chats** to search the current workspace and related Git worktrees, including Agent Manager worktrees. Selecting a session adds its current chat history as context when you send the message. |
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
@@ -93,6 +93,7 @@ When you describe a task, the agent uses its tools — `read`, `grep`, `glob`, a
|
||||
In the terminal-based TUI, you can provide context in several ways:
|
||||
|
||||
- **Type `@` for file autocomplete** — In the TUI, type `@` followed by a filename to get autocomplete suggestions. Selecting a file attaches its contents to your message. You can limit how much is included by appending a line range, e.g. `@src/utils.ts#10-50`.
|
||||
- **Reference a past chat** — Type `@`, choose **Past chats**, then search for and select a session. Kilo adds that chat history as context when you send the message without switching sessions.
|
||||
- **Mention file paths in your message** — Simply refer to files by path in your conversation text (e.g., "look at src/utils.ts") and the agent will read them.
|
||||
- **Use `kilo run -f`** — When using the non-interactive `kilo run` command, pass `-f path/to/file.ts` to explicitly include a file's contents in the context.
|
||||
- **Let the agent find files itself** — The agent has access to `glob` (find files by pattern), `grep` (search file contents), and `read` (read file contents) tools. Describe what you're looking for and it will locate the relevant code.
|
||||
@@ -116,6 +117,7 @@ This means the agent can explore your entire project as needed, rather than bein
|
||||
|---|---|
|
||||
| **Describe the task clearly** | The agent finds context on its own — focus on _what_ you want done rather than _where_ the code is |
|
||||
| **Mention files when helpful** | If you know the exact file, mention its path to save the agent a search step |
|
||||
| **Reference a past chat** | Type `@` and choose **Past chats** to add another session's chat history as context without switching sessions. |
|
||||
| **Use `kilo run -f`** | Pass key files with `-f` when using `kilo run` for immediate context |
|
||||
| **Trust the agent's tools** | The agent can search, read, and explore your codebase — let it do the discovery work |
|
||||
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
---
|
||||
title: "Session History and Search"
|
||||
description: "Find, search, inspect, and resume local Kilo Code sessions"
|
||||
---
|
||||
|
||||
# Session history and search
|
||||
|
||||
Kilo Code keeps local session metadata and chat history so you can resume earlier work. You can search session titles in the history UI or CLI, ask the agent to search chat content, or inspect the local SQLite database directly.
|
||||
|
||||
{% callout type="info" %}
|
||||
Local sessions use SQLite, not PostgreSQL. Use `kilo db` or `sqlite3`, not `psql`, to inspect the local database.
|
||||
{% /callout %}
|
||||
|
||||
## Choose a search method
|
||||
|
||||
Different search surfaces cover different content and scopes:
|
||||
|
||||
| Method | Searches | Scope |
|
||||
|---|---|---|
|
||||
| [VS Code History](#search-session-history-in-vs-code) | Session titles | Sessions loaded in the selected Local or Cloud history view |
|
||||
| [JetBrains History](#search-session-history-in-jetbrains) | Session titles | Sessions loaded in the selected Local or Cloud history view |
|
||||
| [CLI Past chats](#reference-a-past-chat-in-the-cli-tui) | Session titles | Current workspace |
|
||||
| [`kilo session list --search`](#filter-session-titles) | Session titles | Current workspace, or every local workspace with `--all` |
|
||||
| [Ask Kilo to recall past chats](#ask-kilo-to-search-past-chats) | Titles and high-signal chat content | Current workspace and related Git worktrees |
|
||||
| [VS Code chat search](#search-the-open-chat-session) | Rendered content in the open session | Current chat session |
|
||||
| [VS Code Past chats](#add-a-past-chat-to-the-current-session) | Session titles and worktree names | Current workspace and related Git worktrees, including Agent Manager worktrees |
|
||||
| [SQLite query](#query-sqlite-directly) | Any stored field you select | Entire selected local database |
|
||||
|
||||
History and CLI list searches do **not** search message content. To find a string inside prompts or replies, ask Kilo to search past chats or use a direct SQLite query.
|
||||
|
||||
## Search in the UI
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VS Code" %}
|
||||
|
||||
### Search session history in VS Code
|
||||
|
||||
1. Open the Kilo Code sidebar.
|
||||
2. Select the **History** button in the sidebar title bar. You can also select **Show History** below the recent sessions on a new chat.
|
||||
3. Select **Local** or **Cloud**.
|
||||
4. Enter part of a session title in **Search sessions**.
|
||||
5. Select a result to reopen it.
|
||||
|
||||
Local history actions also let you rename, export, or delete a session. Cloud history can be filtered to **Only this repository**.
|
||||
|
||||
The History search is a fuzzy title search over the sessions currently loaded into the view. It does not search prompts or agent replies.
|
||||
|
||||
### Search the open chat session
|
||||
|
||||
Open a session and select the magnifying-glass button in its header. Chat search supports:
|
||||
|
||||
- Previous and next match navigation
|
||||
- Match case
|
||||
- Whole word
|
||||
- Regular expressions
|
||||
|
||||
Kilo loads older messages while the search is active so the search covers the full local chat session, not only the messages initially visible.
|
||||
|
||||
### Add a past chat to the current session
|
||||
|
||||
Type `@` in the chat input, select **Past chats**, and search by session title or worktree name. Selecting a result adds that past chat's content as context when you send the message from your current session. It does not switch or reopen sessions. Past chats includes sessions in the current workspace and related Git worktrees, including Agent Manager worktrees.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="JetBrains" %}
|
||||
|
||||
### Search session history in JetBrains
|
||||
|
||||
1. Open the Kilo Code tool window.
|
||||
2. Select **History** in the tool window toolbar.
|
||||
3. Select **Local** or **Cloud**.
|
||||
4. Enter part of a session title in **Search sessions**.
|
||||
5. Select a result to reopen it.
|
||||
|
||||
You can rename or delete local sessions. In Cloud history, enable **Only this repository** to narrow the list.
|
||||
|
||||
The JetBrains History search matches session titles, not chat content.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI TUI" %}
|
||||
|
||||
### Reference a past chat in the CLI TUI
|
||||
|
||||
1. Run `kilo`.
|
||||
2. Type `@` in the prompt and select **Past chats**.
|
||||
3. Search for a session by title.
|
||||
4. Select a session, finish your message, and send it.
|
||||
|
||||
Kilo adds the selected chat history as context for your message. This does not switch the TUI to the selected session. Use `/sessions`, `/resume`, or `/continue` when you want to load a different session instead.
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## Ask Kilo to search past chats
|
||||
|
||||
The simplest way to search message content across past local sessions is to ask Kilo directly:
|
||||
|
||||
```text
|
||||
Search my local sessions for "database disk image is malformed" and summarize the matching conversations.
|
||||
```
|
||||
|
||||
Kilo's local recall search covers session titles, user and assistant text, file references, and failed tool errors in the current workspace and related Git worktrees. Every query term must occur somewhere in a matching session; exact phrases and user-authored matches rank higher.
|
||||
|
||||
The search includes archived and child sessions. It excludes reasoning, synthetic or ignored text, successful tool output, file contents, and other metadata. Kilo can then read the full chat history for a selected result.
|
||||
|
||||
## Search with CLI commands
|
||||
|
||||
### Filter session titles
|
||||
|
||||
List sessions in the current workspace:
|
||||
|
||||
```bash
|
||||
kilo session list --search "database migration"
|
||||
```
|
||||
|
||||
Search titles across every local workspace and return JSON:
|
||||
|
||||
```bash
|
||||
kilo session list --all --search "database migration" --format json
|
||||
```
|
||||
|
||||
Limit the number of results:
|
||||
|
||||
```bash
|
||||
kilo session list --all --search "migration" --max-count 25
|
||||
```
|
||||
|
||||
`--search` performs a SQLite `LIKE` substring search on the title. It does not inspect messages. Use the returned session ID to resume or export a session:
|
||||
|
||||
```bash
|
||||
kilo --session ses_123
|
||||
kilo run --session ses_123 "Summarize where we stopped"
|
||||
kilo export ses_123 > session.json
|
||||
```
|
||||
|
||||
### Find the local database
|
||||
|
||||
Print the database selected by the current CLI environment:
|
||||
|
||||
```bash
|
||||
kilo db path
|
||||
```
|
||||
|
||||
Stable installations normally use these paths:
|
||||
|
||||
| Environment | Default path |
|
||||
|---|---|
|
||||
| Windows | `%USERPROFILE%\.local\share\kilo\kilo.db` |
|
||||
| macOS | `~/.local/share/kilo/kilo.db` |
|
||||
| Linux | `~/.local/share/kilo/kilo.db` |
|
||||
| VS Code Remote SSH | `~/.local/share/kilo/kilo.db` on the remote machine |
|
||||
|
||||
The actual path can differ when `KILO_DB`, `XDG_DATA_HOME`, a development channel, or an isolated development environment is active. Prefer `kilo db path` over assuming a path.
|
||||
|
||||
## Query SQLite directly
|
||||
|
||||
{% callout type="warning" %}
|
||||
The `kilo db` query command accepts arbitrary SQL, including writes. Use read-only queries, and use `sqlite3 -readonly` for safer interactive inspection. Stop Kilo before copying, replacing, or restoring the database because active transactions may also use `kilo.db-wal` and `kilo.db-shm`.
|
||||
{% /callout %}
|
||||
|
||||
### Use the built-in database command
|
||||
|
||||
List recently updated sessions:
|
||||
|
||||
```bash
|
||||
kilo db "
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
directory,
|
||||
datetime(time_updated / 1000, 'unixepoch') AS updated
|
||||
FROM session
|
||||
ORDER BY time_updated DESC
|
||||
LIMIT 25
|
||||
" --format json
|
||||
```
|
||||
|
||||
Search non-synthetic text parts for a case-insensitive substring:
|
||||
|
||||
```bash
|
||||
kilo db "
|
||||
SELECT
|
||||
s.id AS session_id,
|
||||
s.title,
|
||||
json_extract(m.data, '$.role') AS role,
|
||||
json_extract(p.data, '$.text') AS text
|
||||
FROM session AS s
|
||||
JOIN message AS m ON m.session_id = s.id
|
||||
JOIN part AS p ON p.message_id = m.id
|
||||
WHERE json_valid(p.data)
|
||||
AND json_extract(p.data, '$.type') = 'text'
|
||||
AND coalesce(json_extract(p.data, '$.synthetic'), 0) = 0
|
||||
AND coalesce(json_extract(p.data, '$.ignored'), 0) = 0
|
||||
AND instr(
|
||||
lower(json_extract(p.data, '$.text')),
|
||||
lower('database disk image is malformed')
|
||||
) > 0
|
||||
ORDER BY s.time_updated DESC, m.time_created, p.id
|
||||
" --format json
|
||||
```
|
||||
|
||||
Replace the search string in the second `lower(...)` expression. This query searches ordinary user and assistant text. It intentionally does not reproduce Kilo's recall ranking, workspace scope, file-reference matching, or tool-error matching.
|
||||
|
||||
### Use the SQLite shell
|
||||
|
||||
Install the `sqlite3` command-line tool, then open the selected database in read-only mode:
|
||||
|
||||
```bash
|
||||
sqlite3 -readonly "$(kilo db path)"
|
||||
```
|
||||
|
||||
Useful commands inside the shell include:
|
||||
|
||||
```sql
|
||||
.tables
|
||||
.schema session
|
||||
.schema message
|
||||
.schema part
|
||||
```
|
||||
|
||||
Run the SQL examples above without the surrounding `kilo db` command. Exit with `.quit`.
|
||||
|
||||
`psql` cannot open this file because `psql` speaks the PostgreSQL client protocol, while local Kilo sessions are stored in an embedded SQLite database.
|
||||
|
||||
## Search through the API or SDK
|
||||
|
||||
Run a local server with authentication before exposing its API:
|
||||
|
||||
```bash
|
||||
export KILO_SERVER_PASSWORD='replace-with-a-strong-password'
|
||||
kilo serve --port 4096
|
||||
```
|
||||
|
||||
Search session titles in the current directory:
|
||||
|
||||
```bash
|
||||
curl -u "kilo:$KILO_SERVER_PASSWORD" \
|
||||
--get "http://127.0.0.1:4096/session" \
|
||||
--data-urlencode "directory=$PWD" \
|
||||
--data-urlencode "roots=true" \
|
||||
--data-urlencode "search=migration" \
|
||||
--data-urlencode "limit=50"
|
||||
```
|
||||
|
||||
Read the complete chat history for one result:
|
||||
|
||||
```bash
|
||||
curl -u "kilo:$KILO_SERVER_PASSWORD" \
|
||||
--get "http://127.0.0.1:4096/session/ses_123/message" \
|
||||
--data-urlencode "directory=$PWD"
|
||||
```
|
||||
|
||||
The list endpoint's `search` parameter searches titles only. To search chat content programmatically, list candidate sessions, read their messages, and inspect text parts in your application.
|
||||
|
||||
The JavaScript SDK exposes the same endpoints:
|
||||
|
||||
```ts
|
||||
import { createKiloClient } from "@kilocode/sdk/v2/client"
|
||||
|
||||
const auth = Buffer.from(`kilo:${process.env.KILO_SERVER_PASSWORD}`).toString("base64")
|
||||
const client = createKiloClient({
|
||||
baseUrl: "http://127.0.0.1:4096",
|
||||
directory: process.cwd(),
|
||||
headers: { Authorization: `Basic ${auth}` },
|
||||
})
|
||||
|
||||
const result = await client.session.list(
|
||||
{ roots: true, search: "migration", limit: 50 },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
|
||||
for (const session of result.data) {
|
||||
const history = await client.session.messages(
|
||||
{ sessionID: session.id, limit: 0 },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
|
||||
// Inspect history.data[*].parts for the content your application needs.
|
||||
console.log(session.title, history.data.length)
|
||||
}
|
||||
```
|
||||
|
||||
`limit: 0` requests the complete chat history from the compatibility API. For large sessions, use a positive `limit` and follow the pagination cursor instead.
|
||||
|
||||
## Local and cloud sessions
|
||||
|
||||
Local sessions live in the SQLite database on the machine where Kilo runs. With VS Code Remote SSH, that means the remote host. Cloud sessions and shared-session copies are separate and are not available through local SQLite queries unless you import them locally.
|
||||
|
||||
For database corruption or reset procedures, see [Troubleshooting IDE Extensions](/docs/getting-started/troubleshooting/troubleshooting-extension). Resetting the database removes local session history, so back it up before recovery work.
|
||||
@@ -5,7 +5,7 @@ description: "Share and collaborate on Kilo Code sessions"
|
||||
|
||||
# Sessions & Sharing
|
||||
|
||||
A session is your platform-agnostic interaction with Kilo. It remembers your repository, your task, and the conversation so you can pause and resume work without losing context. Sessions are private to your account by default; you can optionally share a link with others who can read or fork your session.
|
||||
A session is your platform-agnostic interaction with Kilo. It remembers your repository, your task, and the conversation so you can pause and resume work without losing context. Local sessions stay on the machine where Kilo runs. Account-backed cloud sessions are private to your account by default. You can optionally share a session link with others who can read or fork it.
|
||||
|
||||
## What a session keeps for you
|
||||
|
||||
@@ -49,7 +49,9 @@ Forking creates a new session in your account, with its own ID, and copies over
|
||||
|
||||
## Where your session data lives
|
||||
|
||||
To keep sessions fast and resumable, Kilo stores small JSON blobs associated with your session. These include your conversation history and task metadata. If you share a session, Kilo keeps a public copy used by the share link while your private session remains under your account.
|
||||
Kilo stores local session history and metadata in an SQLite database on the machine where Kilo runs. Cloud sessions and shared-session copies use Kilo's cloud services and are separate from the local database. If you share a session, Kilo keeps a public copy for the share link while the original session remains private.
|
||||
|
||||
See [Session History and Search](/docs/code-with-ai/agents/session-history) to find the local database, search session titles or transcripts, and inspect sessions with the CLI or SQLite.
|
||||
|
||||
Good practice:
|
||||
|
||||
|
||||
+5
-3
@@ -27,12 +27,14 @@ If every prompt fails with `SQLiteError: database disk image is malformed`, Kilo
|
||||
|
||||
### Find the database
|
||||
|
||||
The database location depends on where Kilo Code is running:
|
||||
When the kilo CLI uses the same environment as the affected installation, run `kilo db path` to print the selected database. See [Session History and Search](/docs/code-with-ai/agents/session-history) for normal database inspection and search workflows.
|
||||
|
||||
The default database location depends on where Kilo Code is running:
|
||||
|
||||
| Environment | Database path |
|
||||
|---|---|
|
||||
| Windows | `%LOCALAPPDATA%\kilo\kilo.db` |
|
||||
| macOS | `~/Library/Application Support/kilo/kilo.db` |
|
||||
| Windows | `%USERPROFILE%\.local\share\kilo\kilo.db` |
|
||||
| macOS | `~/.local/share/kilo/kilo.db` |
|
||||
| Linux | `~/.local/share/kilo/kilo.db` |
|
||||
| VS Code Remote SSH | `~/.local/share/kilo/kilo.db` on the remote machine |
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ type OpenRouterModel = z.infer<typeof openRouterModelSchema>
|
||||
function parseApiPrice(price: string | null | undefined): number | undefined {
|
||||
if (!price) return undefined
|
||||
const parsed = parseFloat(price)
|
||||
if (isNaN(parsed)) return undefined
|
||||
if (isNaN(parsed) || parsed < 0) return undefined
|
||||
return parsed * 1_000_000 // Convert $/token → $/M tokens
|
||||
}
|
||||
|
||||
|
||||
@@ -383,3 +383,66 @@ test("keeps organization catalog errors from silently falling back to personal m
|
||||
expect(calls[0]).toContain("/api/gateway/transcription-models")
|
||||
expect(headers[0]?.get("X-KILOCODE-ORGANIZATIONID")).toBe("org-1")
|
||||
})
|
||||
|
||||
test("omits cost when pricing contains negative values (dynamic/auto-routed pricing)", async () => {
|
||||
const orig = globalThis.fetch
|
||||
stubFetch(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: "openrouter/auto",
|
||||
name: "Auto Router",
|
||||
context_length: 128000,
|
||||
max_completion_tokens: 16384,
|
||||
architecture: {
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
supported_parameters: ["tools"],
|
||||
pricing: {
|
||||
prompt: "-1",
|
||||
completion: "-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "test/fixed-price",
|
||||
name: "Fixed Price Model",
|
||||
context_length: 128000,
|
||||
max_completion_tokens: 16384,
|
||||
architecture: {
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
supported_parameters: ["tools"],
|
||||
pricing: {
|
||||
prompt: "0.000003",
|
||||
completion: "0.000015",
|
||||
input_cache_read: "0.0000003",
|
||||
input_cache_write: "-1",
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const result = await fetchKiloModels({})
|
||||
|
||||
;(globalThis as any).fetch = orig
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.models["openrouter/auto"]).toBeDefined()
|
||||
expect(result.models["openrouter/auto"].cost).toBeUndefined()
|
||||
expect(result.models["test/fixed-price"].cost).toEqual({
|
||||
input: 3,
|
||||
output: 15,
|
||||
cache_read: 0.3,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
+3
-3
@@ -157,14 +157,14 @@ internal class ModelDetailsPanel(
|
||||
return buildList {
|
||||
item.releaseDate?.let { add(KiloBundle.message("model.picker.details.released") to date(it)) }
|
||||
if (!item.free) {
|
||||
item.cost?.let { cost ->
|
||||
item.cost?.takeIf { it.input > 0.0 || it.output > 0.0 }?.let { cost ->
|
||||
add(KiloBundle.message("model.picker.details.input") to price(cost.input))
|
||||
add(KiloBundle.message("model.picker.details.output") to price(cost.output))
|
||||
add(KiloBundle.message("model.picker.details.cached") to cached(cost.input, cost.cache?.read))
|
||||
add(KiloBundle.message("model.picker.details.average") to price(average(cost.input, cost.output, cost.cache?.read)))
|
||||
} ?: run {
|
||||
item.inputPrice?.let { add(KiloBundle.message("model.picker.details.input") to price(it)) }
|
||||
item.outputPrice?.let { add(KiloBundle.message("model.picker.details.output") to price(it)) }
|
||||
item.inputPrice?.takeIf { it > 0.0 }?.let { add(KiloBundle.message("model.picker.details.input") to price(it)) }
|
||||
item.outputPrice?.takeIf { it > 0.0 }?.let { add(KiloBundle.message("model.picker.details.output") to price(it)) }
|
||||
}
|
||||
}
|
||||
ctx?.let { add(KiloBundle.message("model.picker.details.context") to context(it)) }
|
||||
|
||||
@@ -126,7 +126,7 @@ export namespace MemoryOperations {
|
||||
const adds = input.ops
|
||||
.filter((item): item is Add => item.action === "add")
|
||||
// Redact rejected text too: this filter runs before the secret one, so a rejected op never
|
||||
// reaches it, and skips flow into the persistent decisions audit (/memory/show, TUI).
|
||||
// reaches it, and skips remain visible in the operation result.
|
||||
.filter((op) => {
|
||||
const item = reject(op)
|
||||
if (!item) return true
|
||||
@@ -314,7 +314,7 @@ export namespace MemoryOperations {
|
||||
}
|
||||
}
|
||||
|
||||
async function persist(input: { root: string; state: MemorySchema.State; count: number; removed: number }) {
|
||||
async function persist(input: { root: string; state: MemorySchema.State; count: number }) {
|
||||
const index = await MemoryIndexer.rebuild({ root: input.root, state: input.state })
|
||||
await MemoryFiles.writeState(input.root, {
|
||||
...input.state,
|
||||
@@ -323,7 +323,6 @@ export namespace MemoryOperations {
|
||||
lastOperationCount: input.count,
|
||||
},
|
||||
})
|
||||
await MemoryFiles.append(input.root, `apply ops=${input.count} removed=${input.removed}`)
|
||||
return index
|
||||
}
|
||||
|
||||
@@ -365,9 +364,9 @@ export namespace MemoryOperations {
|
||||
const prepared = prepare({ state, ops: input.ops, max: state.limits.maxLineChars })
|
||||
const removes = input.ops.filter((item): item is Remove => item.action === "remove")
|
||||
const plan = planOps({ docs, inventory, removes, adds: prepared.adds, now: Date.now() })
|
||||
// Commit (IO): write changed documents, then rebuild the index, persist state, and audit.
|
||||
// Commit (IO): write changed documents, then rebuild the index and persist state.
|
||||
await writeDocs({ root: input.root, plan })
|
||||
const index = await persist({ root: input.root, state, count: plan.count, removed: plan.removed })
|
||||
const index = await persist({ root: input.root, state, count: plan.count })
|
||||
return {
|
||||
operationCount: plan.count,
|
||||
added: plan.added,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { MemoryOperations } from "./operations"
|
||||
import { MemoryRedact } from "./redact"
|
||||
import { MemoryShared } from "../recall/shared"
|
||||
import { TRANSIENT } from "../schema"
|
||||
import type { MemoryFiles } from "../storage/store"
|
||||
import type { CaptureSkip } from "./parse"
|
||||
|
||||
export { TRANSIENT }
|
||||
|
||||
export type CaptureSourceItem = {
|
||||
id: string
|
||||
text: string
|
||||
@@ -69,14 +72,75 @@ export function errorReason(err: unknown) {
|
||||
return MemoryShared.brief(MemoryRedact.text(parts.join(" ")), 500)
|
||||
}
|
||||
|
||||
export function guardReason(input: string) {
|
||||
const value = input.toLowerCase()
|
||||
if (/\b(429|rate[_ -]?limit|too many requests)\b/.test(value)) return "rate_limit_guard"
|
||||
if (/\b(insufficient[_ -]?quota|quota exceeded|exceeded your quota|billing|credits?|credit balance)\b/.test(value))
|
||||
const RATE_TEXT = /\b(429|rate[_ -]?limit|too many requests)\b/
|
||||
const QUOTA_TEXT = /\b(insufficient[_ -]?quota|quota exceeded|exceeded your quota|billing|credits?|credit balance)\b/
|
||||
const TIMEOUT_TEXT =
|
||||
/\b(timeouterror|etimedout|deadline[ _-]?exceeded|timed out|(connect|headers|body|gateway)[ _-]?time[ -]?out)\b|^timeout$/
|
||||
const TIMEOUT_CODES = new Set(["ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"])
|
||||
|
||||
/** Structural timeout detection over an error's name/status/code, its message text, and its nested
|
||||
* cause/errors chains (cycle-safe). Message matching is limited to `message` — never the serialized
|
||||
* body/data blobs — so response payloads that merely mention "timeout" cannot classify as transient. */
|
||||
function timedOut(item: unknown, seen = new WeakSet<object>()): boolean {
|
||||
if (!item || typeof item !== "object" || seen.has(item)) return false
|
||||
seen.add(item)
|
||||
const err = item as {
|
||||
name?: unknown
|
||||
code?: unknown
|
||||
status?: unknown
|
||||
statusCode?: unknown
|
||||
message?: unknown
|
||||
cause?: unknown
|
||||
errors?: unknown
|
||||
}
|
||||
if (err.name === "TimeoutError") return true
|
||||
if (err.status === 504 || err.statusCode === 504) return true
|
||||
if (typeof err.code === "string" && TIMEOUT_CODES.has(err.code)) return true
|
||||
if (typeof err.message === "string" && TIMEOUT_TEXT.test(err.message.toLowerCase())) return true
|
||||
if (timedOut(err.cause, seen)) return true
|
||||
return Array.isArray(err.errors) && err.errors.some((entry) => timedOut(entry, seen))
|
||||
}
|
||||
|
||||
function rateGuarded(item: unknown, seen = new WeakSet<object>()): boolean {
|
||||
if (!item || typeof item !== "object" || seen.has(item)) return false
|
||||
seen.add(item)
|
||||
const err = item as {
|
||||
status?: unknown
|
||||
statusCode?: unknown
|
||||
message?: unknown
|
||||
cause?: unknown
|
||||
errors?: unknown
|
||||
}
|
||||
if (err.status === 429 || err.statusCode === 429) return true
|
||||
if (typeof err.message === "string" && RATE_TEXT.test(err.message.toLowerCase())) return true
|
||||
if (rateGuarded(err.cause, seen)) return true
|
||||
return Array.isArray(err.errors) && err.errors.some((entry) => rateGuarded(entry, seen))
|
||||
}
|
||||
|
||||
function quotaGuarded(item: unknown, seen = new WeakSet<object>()): boolean {
|
||||
if (!item || typeof item !== "object" || seen.has(item)) return false
|
||||
seen.add(item)
|
||||
const err = item as {
|
||||
message?: unknown
|
||||
cause?: unknown
|
||||
errors?: unknown
|
||||
}
|
||||
if (typeof err.message === "string" && QUOTA_TEXT.test(err.message.toLowerCase())) return true
|
||||
if (quotaGuarded(err.cause, seen)) return true
|
||||
return Array.isArray(err.errors) && err.errors.some((entry) => quotaGuarded(entry, seen))
|
||||
}
|
||||
|
||||
export function guardReason(input: unknown) {
|
||||
const value = (typeof input === "string" ? input : errorReason(input)).toLowerCase()
|
||||
if (typeof input === "string" ? RATE_TEXT.test(value) : rateGuarded(input) || RATE_TEXT.test(value))
|
||||
return "rate_limit_guard"
|
||||
if (typeof input === "string" ? QUOTA_TEXT.test(value) : quotaGuarded(input) || QUOTA_TEXT.test(value))
|
||||
return "quota_guard"
|
||||
if (typeof input === "string" ? TIMEOUT_TEXT.test(value) : timedOut(input)) return TRANSIENT
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
export function skipped(input: { sessionID: string; reason: string }): MemoryFiles.Decision {
|
||||
return {
|
||||
kind: "typed",
|
||||
@@ -94,6 +158,7 @@ export function skipped(input: { sessionID: string; reason: string }): MemoryFil
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
export function auditOps(ops: MemoryOperations.Op[]) {
|
||||
return MemoryShared.audit(ops)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Cause, Effect } from "effect"
|
||||
import {
|
||||
auditOps,
|
||||
cap,
|
||||
capturePlan,
|
||||
digestPrompt,
|
||||
@@ -18,9 +17,9 @@ import {
|
||||
parseJson,
|
||||
parseOps,
|
||||
salvageTyped,
|
||||
skipped,
|
||||
summarize,
|
||||
summarizeDiffs,
|
||||
TRANSIENT,
|
||||
typedPrompt,
|
||||
usage,
|
||||
verifySkips,
|
||||
@@ -105,11 +104,16 @@ export namespace MemoryCapture {
|
||||
yield* memory.prepare({ root })
|
||||
const state = yield* memory.state({ root })
|
||||
const reported = new Set<string>()
|
||||
const fail = (reason: string) =>
|
||||
const fail = (reason: string, detail?: string) =>
|
||||
Effect.promise(async () => {
|
||||
const safe = MemoryRedact.text(reason)
|
||||
if (reported.has(safe)) return
|
||||
reported.add(safe)
|
||||
if (reason === TRANSIENT)
|
||||
MemoryLog.warn("memory capture transient failure", {
|
||||
reason: safe,
|
||||
detail: MemoryShared.brief(MemoryRedact.text(detail ?? safe), 160),
|
||||
})
|
||||
await MemoryEvents.publish({
|
||||
event: "error",
|
||||
payload: MemoryEvents.status({
|
||||
@@ -123,7 +127,6 @@ export namespace MemoryCapture {
|
||||
})
|
||||
const skip = (reason: string, opts?: { idleFlush?: boolean }) =>
|
||||
Effect.gen(function* () {
|
||||
if (state.enabled) yield* memory.decide({ root, decision: skipped({ sessionID: input.sessionID, reason }) })
|
||||
yield* Effect.promise(() =>
|
||||
MemoryEvents.publish({
|
||||
event: "status",
|
||||
@@ -203,23 +206,6 @@ export namespace MemoryCapture {
|
||||
tokens: 0,
|
||||
fallback: true,
|
||||
})
|
||||
yield* memory.decide({
|
||||
root,
|
||||
decision: {
|
||||
kind: "digest",
|
||||
trigger: "turn-close",
|
||||
sessionID: input.sessionID,
|
||||
result: "fallback",
|
||||
llm: false,
|
||||
parsed: false,
|
||||
fallback: true,
|
||||
reason: input.reason,
|
||||
tokens: 0,
|
||||
operationCount: 1,
|
||||
skippedCount: 0,
|
||||
summary: `session digest fallback on ${input.reason ?? "close"}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
return yield* skip(plan.skipReason, plan.idleFlush ? { idleFlush: true } : undefined)
|
||||
}
|
||||
@@ -232,22 +218,12 @@ export namespace MemoryCapture {
|
||||
|
||||
const model =
|
||||
digestDue || typedCall
|
||||
? yield* Effect.gen(function* () {
|
||||
const resolution = yield* input.model.resolve({
|
||||
? (
|
||||
yield* input.model.resolve({
|
||||
configured: input.memoryModel,
|
||||
session: view.sessionModel,
|
||||
})
|
||||
if (resolution.fallback) {
|
||||
yield* memory.append({
|
||||
root,
|
||||
text: `memory_model_config reason=${MemoryShared.brief(
|
||||
MemoryRedact.text(resolution.fallback.reason),
|
||||
160,
|
||||
)} fallback=1`,
|
||||
})
|
||||
}
|
||||
return resolution.handle
|
||||
})
|
||||
).handle
|
||||
: undefined
|
||||
const digestEffect = digestDue
|
||||
? Effect.gen(function* () {
|
||||
@@ -277,9 +253,8 @@ export namespace MemoryCapture {
|
||||
Effect.gen(function* () {
|
||||
if (signal.aborted) return { ok: false as const, reason: "cancelled" }
|
||||
const raw = errorReason(err)
|
||||
const reason = MemoryRedact.text(guardReason(raw) ?? raw)
|
||||
yield* fail(reason)
|
||||
yield* memory.append({ root, text: `digest error=${MemoryShared.brief(reason, 160)} fallback=1` })
|
||||
const reason = MemoryRedact.text(guardReason(err) ?? raw)
|
||||
yield* fail(reason, raw)
|
||||
return { ok: false as const, reason }
|
||||
}),
|
||||
),
|
||||
@@ -296,14 +271,9 @@ export namespace MemoryCapture {
|
||||
try: () => parseJson(digestSchema, result.result.text),
|
||||
catch: (error) => error,
|
||||
}).pipe(
|
||||
Effect.catch((err: unknown) =>
|
||||
Effect.catch(() =>
|
||||
Effect.gen(function* () {
|
||||
const reason = MemoryRedact.text(errorReason(err))
|
||||
yield* fail("digest parse_error")
|
||||
yield* memory.append({
|
||||
root,
|
||||
text: `digest parse_error=${MemoryShared.brief(reason, 160)} fallback=1`,
|
||||
})
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
@@ -402,9 +372,8 @@ export namespace MemoryCapture {
|
||||
Effect.gen(function* () {
|
||||
if (signal.aborted) return { ok: false as const, reason: "cancelled" }
|
||||
const raw = errorReason(err)
|
||||
const reason = MemoryRedact.text(guardReason(raw) ?? raw)
|
||||
yield* fail(reason)
|
||||
yield* memory.append({ root, text: `consolidate error=${MemoryShared.brief(reason, 160)}` })
|
||||
const reason = MemoryRedact.text(guardReason(err) ?? raw)
|
||||
yield* fail(reason, raw)
|
||||
return { ok: false as const, reason }
|
||||
}),
|
||||
),
|
||||
@@ -423,11 +392,9 @@ export namespace MemoryCapture {
|
||||
try: () => salvageTyped(result.result.text),
|
||||
catch: (error) => error,
|
||||
}).pipe(
|
||||
Effect.catch((err: unknown) =>
|
||||
Effect.catch(() =>
|
||||
Effect.gen(function* () {
|
||||
const reason = MemoryRedact.text(errorReason(err))
|
||||
yield* fail("consolidate parse_error")
|
||||
yield* memory.append({ root, text: `consolidate parse_error=${MemoryShared.brief(reason, 160)}` })
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
@@ -475,30 +442,6 @@ export namespace MemoryCapture {
|
||||
fallback: Boolean(digest.reason),
|
||||
})
|
||||
}
|
||||
if (digestDue) {
|
||||
yield* memory.decide({
|
||||
root,
|
||||
decision: {
|
||||
kind: "digest",
|
||||
trigger: "turn-close",
|
||||
sessionID: input.sessionID,
|
||||
result: digest.reason ? "fallback" : digest.summary ? "saved" : "skipped",
|
||||
llm: true,
|
||||
parsed: Boolean(digest.summary && !digest.reason),
|
||||
fallback: Boolean(digest.reason),
|
||||
reason: digest.reason,
|
||||
tokens: digest.tokens,
|
||||
operationCount: digest.summary ? 1 : 0,
|
||||
skippedCount: digest.summary ? 0 : 1,
|
||||
summary: digest.reason
|
||||
? `session digest used fallback after ${digest.reason}`
|
||||
: digest.summary
|
||||
? "session digest saved"
|
||||
: "session digest skipped",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Apply adds only: a same-key add supersedes/updates an existing fact in place. reconcile also
|
||||
// surfaces exact-key auto-removes, but V0 keeps hard removes explicit-only — auto-capture never
|
||||
// deletes memory it merely paraphrased (or wrongly flags), so reconciled.removes is not applied.
|
||||
@@ -506,39 +449,11 @@ export namespace MemoryCapture {
|
||||
const ops = reconciled.ops.slice(0, state.capture.maxOpsPerRun)
|
||||
const project =
|
||||
ops.length > 0 ? yield* memory.apply({ root, ops, trigger: "turn-close", tokens: generated.tokens }) : undefined
|
||||
// Apply-time skips (content gate + secret, both redacted at creation) surface in the typed audit
|
||||
// record alongside the model's own declared skips.
|
||||
const applied: CaptureSkip[] = [...generated.skipped, ...(project?.skipped ?? [])]
|
||||
const count = project?.operationCount ?? 0
|
||||
if (typedCall) {
|
||||
yield* memory.decide({
|
||||
root,
|
||||
decision: {
|
||||
kind: "typed",
|
||||
trigger: "turn-close",
|
||||
sessionID: input.sessionID,
|
||||
result: generated.fallback ? "fallback" : count > 0 ? "saved" : "skipped",
|
||||
llm: true,
|
||||
parsed: !generated.fallback,
|
||||
fallback: generated.fallback,
|
||||
reason: generated.reason,
|
||||
tokens: generated.tokens,
|
||||
operationCount: count,
|
||||
skippedCount: applied.length,
|
||||
skipped: applied,
|
||||
operations: auditOps(ops),
|
||||
files: [...new Set(ops.flatMap((item) => (item.action === "add" && item.file ? [item.file] : [])))],
|
||||
summary: generated.fallback
|
||||
? `typed consolidation skipped after ${generated.reason ?? "model failure"}`
|
||||
: count > 0
|
||||
? `typed consolidation saved ${count} ops`
|
||||
: `typed consolidation skipped ${applied.length} candidates`,
|
||||
},
|
||||
})
|
||||
}
|
||||
const tokens = digest.tokens + generated.tokens
|
||||
if (!digest.summary && !typedCall && count === 0) return yield* skip("no_ops")
|
||||
if ((digestDue || typedCall || count > 0) && (!typedCall || !generated.fallback)) {
|
||||
if (digestDue || typedCall || count > 0) {
|
||||
yield* memory.commit({
|
||||
root,
|
||||
now,
|
||||
@@ -546,8 +461,7 @@ export namespace MemoryCapture {
|
||||
tokens,
|
||||
count,
|
||||
digest: Boolean(digest.summary),
|
||||
typed: typedCall,
|
||||
skipped: applied,
|
||||
typed: typedCall && !generated.fallback,
|
||||
})
|
||||
}
|
||||
const updated = yield* memory.state({ root })
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context, Effect, Layer, Semaphore } from "effect"
|
||||
import { skipLine, type CaptureSkip } from "../capture/capture"
|
||||
import type { Memory } from "../memory"
|
||||
import type { MemoryOperations } from "../capture/operations"
|
||||
import { MemoryRecall } from "../recall/recall"
|
||||
@@ -104,7 +103,6 @@ type CommitInput = RootInput & {
|
||||
// shared typed-interval clock (lastTypedConsolidationAt); a digest-only commit must leave it untouched so a
|
||||
// digest in one session cannot throttle another session's typed capture.
|
||||
typed: boolean
|
||||
skipped: CaptureSkip[]
|
||||
cost?: number
|
||||
}
|
||||
|
||||
@@ -156,10 +154,12 @@ export namespace MemoryService {
|
||||
readonly recent: (
|
||||
input: RecentInput,
|
||||
) => Effect.Effect<Awaited<ReturnType<typeof MemoryFiles.recentSessions>>, Failure>
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
readonly append: (input: AppendInput) => Effect.Effect<void, Failure>
|
||||
readonly index: (input: RootInput) => Effect.Effect<Index, Failure>
|
||||
readonly commit: (input: CommitInput) => Effect.Effect<void, Failure>
|
||||
readonly recordRecall: (input: RecordRecallInput) => Effect.Effect<void, Failure>
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
readonly decide: (input: DecideInput) => Effect.Effect<void, Failure>
|
||||
readonly readSource: (input: ReadSourceInput) => Effect.Effect<string, Failure>
|
||||
readonly turnLock: (sessionID: SessionID) => Semaphore.Semaphore
|
||||
@@ -200,7 +200,7 @@ export namespace MemoryService {
|
||||
return Object.fromEntries(entries) as Sources
|
||||
}),
|
||||
recent: (input) => bridge(() => MemoryFiles.recentSessions(input.root, input.limit, input.max)),
|
||||
append: (input) => bridge(() => MemoryFiles.append(input.root, input.text)),
|
||||
append: () => Effect.void,
|
||||
index: (input) =>
|
||||
bridge(async () => {
|
||||
const text = await MemoryFiles.readIndex(input.root)
|
||||
@@ -219,20 +219,11 @@ export namespace MemoryService {
|
||||
lastSessionSavedAt: input.digest ? input.now : state.stats.lastSessionSavedAt,
|
||||
lastConsolidatedMessageID: input.messageID,
|
||||
lastConsolidationCost: input.cost ?? state.stats.lastConsolidationCost,
|
||||
lastConsolidationTokens: input.tokens,
|
||||
lastOperationCount: input.count,
|
||||
lastConsolidationTokens:
|
||||
input.typed || input.digest ? input.tokens : state.stats.lastConsolidationTokens,
|
||||
lastOperationCount: input.typed ? input.count : state.stats.lastOperationCount,
|
||||
},
|
||||
})
|
||||
const skip = skipLine(input.skipped)
|
||||
await MemoryFiles.append(
|
||||
input.root,
|
||||
[
|
||||
`consolidate trigger=turn-close digest=${input.digest ? 1 : 0} ops=${input.count} tokens=${input.tokens}`,
|
||||
skip,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
)
|
||||
}),
|
||||
),
|
||||
recordRecall: (input) =>
|
||||
@@ -266,7 +257,7 @@ export namespace MemoryService {
|
||||
}),
|
||||
})
|
||||
}),
|
||||
decide: (input) => bridge(() => MemoryFiles.decide(input.root, input.decision)),
|
||||
decide: () => Effect.void,
|
||||
readSource: (input) => bridge(() => MemoryFiles.readSource(input.root, input.file)),
|
||||
// Ref-counted so every acquirer — in-flight or queued behind `withPermits` — shares one
|
||||
// semaphore. Each call must be balanced by exactly one `dropLock`.
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import { MemoryShared } from "./recall/shared"
|
||||
import type { MemoryOperations } from "./capture/operations"
|
||||
import { MemoryRedact } from "./capture/redact"
|
||||
|
||||
/** Human-facing messages and audit views describing an explicit apply result. */
|
||||
/** Human-facing messages describing an explicit apply result. */
|
||||
export namespace MemoryNotice {
|
||||
export function saved(input: { added: number; removed: number }) {
|
||||
return input.removed > 0 || input.added > 0
|
||||
}
|
||||
|
||||
export function summary(input: { added: number; removed: number; count: number }) {
|
||||
if (input.added > 0 && input.removed > 0) {
|
||||
return `explicit memory operation saved ${input.added} and removed ${input.removed}`
|
||||
}
|
||||
if (input.added > 0) return `explicit memory operation saved ${input.added} ops`
|
||||
if (input.removed > 0) return `explicit memory operation removed ${input.removed} entries`
|
||||
if (input.count > 0) return "explicit memory operation matched no source memory"
|
||||
return "explicit memory operation had no accepted ops"
|
||||
}
|
||||
|
||||
export function message(input: { ops: MemoryOperations.Op[]; added: number; removed: number; count: number }) {
|
||||
const refs = MemoryShared.refs(input.ops)
|
||||
if (input.added > 0 && input.removed > 0) return `Memory updated · ${input.added} saved, ${input.removed} removed`
|
||||
@@ -25,15 +14,4 @@ export namespace MemoryNotice {
|
||||
if (input.removed > 0) return `Memory updated · ${input.removed} removed`
|
||||
return `Memory unchanged · ${input.count} ops`
|
||||
}
|
||||
|
||||
export function skip(input: MemoryOperations.Rejection[]) {
|
||||
return input.map((item) => (item.reason === "out_of_scope" ? { reason: item.reason } : item))
|
||||
}
|
||||
|
||||
export function ops(input: { ops: MemoryOperations.Op[]; skipped: MemoryOperations.Rejection[] }) {
|
||||
const blocked = new Set(input.skipped.filter((item) => item.reason === "out_of_scope").map((item) => item.text))
|
||||
return MemoryShared.audit(
|
||||
input.ops.filter((item) => item.action !== "add" || !blocked.has(MemoryRedact.text(item.text))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { MemorySchema } from "./schema"
|
||||
import { MemoryShared } from "./recall/shared"
|
||||
import { MemoryToken } from "./recall/token"
|
||||
import { MemorySlug } from "./slug"
|
||||
import { MemoryRedact } from "./capture/redact"
|
||||
|
||||
/** Root-bound package facade. External Kilo surfaces should derive root from workspace context first. */
|
||||
export namespace Memory {
|
||||
@@ -97,7 +96,6 @@ export namespace Memory {
|
||||
const state = await MemoryFiles.readState(input.root)
|
||||
const next = { ...state, enabled: false }
|
||||
await MemoryFiles.writeState(input.root, next)
|
||||
await MemoryFiles.append(input.root, `disable ${next.scope} source=command`)
|
||||
return { root: input.root, state: next }
|
||||
})
|
||||
}
|
||||
@@ -124,16 +122,6 @@ export namespace Memory {
|
||||
...(input.settings.verbose === undefined ? {} : { verbose: input.settings.verbose }),
|
||||
}
|
||||
await MemoryFiles.writeState(input.root, next)
|
||||
await MemoryFiles.append(
|
||||
input.root,
|
||||
[
|
||||
`settings ${next.scope}`,
|
||||
input.settings.autoConsolidate === undefined ? "" : `autoConsolidate=${next.autoConsolidate}`,
|
||||
input.settings.verbose === undefined ? "" : `verbose=${next.verbose}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
)
|
||||
return { root: input.root, state: next }
|
||||
})
|
||||
}
|
||||
@@ -216,24 +204,6 @@ export namespace Memory {
|
||||
}
|
||||
const state = await MemoryFiles.readState(input.root)
|
||||
const ok = MemoryNotice.saved({ added: result.added, removed: result.removed })
|
||||
if (trigger === "explicit") {
|
||||
await MemoryFiles.decide(input.root, {
|
||||
kind: "typed",
|
||||
trigger,
|
||||
sessionID: input.sessionID,
|
||||
result: ok ? "saved" : "skipped",
|
||||
llm: false,
|
||||
parsed: true,
|
||||
fallback: false,
|
||||
tokens: input.tokens ?? 0,
|
||||
operationCount: result.operationCount,
|
||||
skippedCount: result.skipped.length || (ok ? 0 : 1),
|
||||
skipped: MemoryNotice.skip(result.skipped),
|
||||
operations: MemoryNotice.ops({ ops: accepted, skipped: result.skipped }),
|
||||
files: MemoryShared.files(accepted),
|
||||
summary: MemoryNotice.summary({ added: result.added, removed: result.removed, count: result.operationCount }),
|
||||
})
|
||||
}
|
||||
return {
|
||||
root: input.root,
|
||||
state,
|
||||
@@ -318,31 +288,6 @@ export namespace Memory {
|
||||
const hits = result?.hits ?? []
|
||||
const files = [...new Set(hits.map((hit) => hit.source))]
|
||||
const topics = [...new Set(hits.flatMap((hit) => (hit.topics?.length ? hit.topics : [hit.kind])))]
|
||||
await MemoryFiles.decide(input.root, {
|
||||
kind: "recall",
|
||||
trigger: "targeted-recall",
|
||||
sessionID: input.sessionID,
|
||||
result: result ? "recalled" : "skipped",
|
||||
llm: false,
|
||||
parsed: false,
|
||||
fallback: false,
|
||||
reason: result ? undefined : "no_matches",
|
||||
query: MemoryShared.brief(MemoryRedact.text(input.query), 240),
|
||||
topics,
|
||||
files,
|
||||
tokens: result?.tokens ?? 0,
|
||||
operationCount: hits.length,
|
||||
skippedCount: result ? 0 : 1,
|
||||
summary: result ? `targeted recall matched ${hits.length} memories` : "targeted recall found no matches",
|
||||
})
|
||||
if (result) {
|
||||
await MemoryFiles.queue(input.root, async () => {
|
||||
await MemoryFiles.append(
|
||||
input.root,
|
||||
`recall session=${input.sessionID ?? ""} hits=${result.hits.length} tokens=${result.tokens} files=${files.join(",")}`,
|
||||
)
|
||||
})
|
||||
}
|
||||
return { root: input.root, state, result, hits, files, topics }
|
||||
}
|
||||
|
||||
@@ -368,10 +313,6 @@ export namespace Memory {
|
||||
})
|
||||
await MemoryFiles.pruneSessions(input.root, state.limits.maxSessionFiles)
|
||||
const index = await MemoryIndexer.rebuild({ root: input.root, state })
|
||||
await MemoryFiles.append(
|
||||
input.root,
|
||||
`session digest session=${input.sessionID} tokens=${input.tokens ?? 0} indexTokens=${index.tokens}`,
|
||||
)
|
||||
return { root: input.root, state, skipped: false as const, index }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -226,7 +226,6 @@ export namespace MemoryIndexer {
|
||||
return MemoryFiles.queue(input.root, async () => {
|
||||
const result = await build(input)
|
||||
await MemoryFiles.writeIndex(input.root, result.text)
|
||||
await MemoryFiles.append(input.root, `regenerate index.kmem bytes=${result.bytes} tokens=${result.tokens}`)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ export namespace MemoryShared {
|
||||
]
|
||||
}
|
||||
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
export function audit(ops: MemoryOperations.Op[]) {
|
||||
return ops.map((item) =>
|
||||
item.action === "add"
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/** Wire contract for suppressible background-capture failures: guardReason emits this as the
|
||||
* `memory.error` reason, and every client surface (TUI toast filter, VS Code provider) matches
|
||||
* against this same constant rather than a local literal. Lives here (dependency-free module)
|
||||
* so clients can import it without pulling the capture pipeline into their bundles. */
|
||||
export const TRANSIENT = "transient" as const
|
||||
|
||||
export namespace MemorySchema {
|
||||
export const VERSION = 1
|
||||
export const maxStoredDigestSummary = 4_000
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import z from "zod"
|
||||
import { MemoryFs } from "./fs"
|
||||
|
||||
/** Compatibility facade for the removed memory audit log. */
|
||||
export namespace MemoryAudit {
|
||||
const Log = z
|
||||
.object({
|
||||
kind: z.literal("log"),
|
||||
summary: z.string(),
|
||||
time: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export type Decision =
|
||||
| {
|
||||
kind: "log"
|
||||
@@ -43,18 +33,14 @@ export namespace MemoryAudit {
|
||||
}[]
|
||||
}
|
||||
|
||||
function audit(root: string, input: Decision) {
|
||||
void root
|
||||
void input
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export async function append(root: string, text: string) {
|
||||
await audit(root, { kind: "log", result: "logged", summary: text })
|
||||
void root
|
||||
void text
|
||||
}
|
||||
|
||||
export async function decide(root: string, input: Decision) {
|
||||
await audit(root, input)
|
||||
void root
|
||||
void input
|
||||
}
|
||||
|
||||
export async function readDecisions(root: string) {
|
||||
@@ -62,24 +48,8 @@ export namespace MemoryAudit {
|
||||
return ""
|
||||
}
|
||||
|
||||
function record(input: string) {
|
||||
try {
|
||||
const data = JSON.parse(input)
|
||||
const parsed = Log.safeParse(data)
|
||||
return parsed.success ? parsed.data : undefined
|
||||
} catch (error) {
|
||||
if (MemoryFs.parse(error)) return undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChanges(root: string) {
|
||||
const lines = (await readDecisions(root)).split("\n").flatMap((line) => {
|
||||
const data = record(line)
|
||||
if (!data) return []
|
||||
const time = data.time ?? ""
|
||||
return [`${time} ${data.summary}`.trim()]
|
||||
})
|
||||
return lines.join("\n")
|
||||
void root
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { readdir, rm } from "fs/promises"
|
||||
import path from "path"
|
||||
import { MemoryAudit } from "./audit"
|
||||
import { MemoryFs } from "./fs"
|
||||
import { MemoryMarkdown } from "./markdown"
|
||||
import { MemoryPaths } from "./paths"
|
||||
@@ -19,13 +18,10 @@ export namespace MemoryState {
|
||||
"corrections.md": "# Corrective Memory\n\n## Corrections\n",
|
||||
}
|
||||
|
||||
async function recover(root: string, file: string, error: unknown) {
|
||||
async function recover(root: string, file: string) {
|
||||
await MemoryFs.backup(file)
|
||||
const state = MemorySchema.missing()
|
||||
await writeState(root, state)
|
||||
await MemoryAudit.append(root, `recover state.json error=${MemoryFs.brief(error)}`).catch((err: unknown) =>
|
||||
MemoryFs.warn("failed to audit memory state recovery", { err, root }),
|
||||
)
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -33,14 +29,14 @@ export namespace MemoryState {
|
||||
const file = MemoryPaths.files(root).state
|
||||
const data = await MemoryFs.json(file).catch(async (error: unknown) => {
|
||||
if (MemoryFs.miss(error)) return undefined
|
||||
if (MemoryFs.parse(error)) return recover(root, file, error)
|
||||
if (MemoryFs.parse(error)) return recover(root, file)
|
||||
throw error
|
||||
})
|
||||
if (data === undefined) return MemorySchema.missing()
|
||||
return Promise.resolve()
|
||||
.then(() => MemorySchema.parse(data))
|
||||
.catch((error: unknown) => {
|
||||
if (MemoryFs.parse(error)) return recover(root, file, error)
|
||||
if (MemoryFs.parse(error)) return recover(root, file)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
@@ -199,7 +195,6 @@ export namespace MemoryState {
|
||||
? { ...(await readState(root)), enabled: true, autoInject: true }
|
||||
: { ...MemorySchema.create(), enabled: true }
|
||||
await writeState(root, state)
|
||||
await MemoryAudit.append(root, "enable project source=command")
|
||||
return state
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { MemoryError, type MemoryError as Failure } from "./effect/errors"
|
||||
import { MemoryPaths } from "./effect/paths"
|
||||
import { MemoryService } from "./effect/service"
|
||||
import { MemoryRecall } from "./recall/recall"
|
||||
import { MemoryToken } from "./recall/token"
|
||||
import { MemorySchema } from "./schema"
|
||||
import recallDescription from "./prompts/tool-memory-recall.txt"
|
||||
import saveDescription from "./prompts/tool-memory-save.txt"
|
||||
@@ -115,50 +114,6 @@ export namespace MemoryTool {
|
||||
}
|
||||
}
|
||||
|
||||
function audit(
|
||||
memory: MemoryService.Interface,
|
||||
input: {
|
||||
root: string
|
||||
params: RecallParams
|
||||
current: string
|
||||
hits: MemoryRecall.Hit[]
|
||||
skipped?: string
|
||||
output: string
|
||||
},
|
||||
) {
|
||||
const files = [...new Set(input.hits.map((hit) => hit.source))]
|
||||
const topics = [...new Set(input.hits.flatMap((hit) => (hit.topics?.length ? hit.topics : [hit.kind])))]
|
||||
const query =
|
||||
input.params.query ??
|
||||
(input.params.sessionID
|
||||
? `sessionID=${input.params.sessionID}`
|
||||
: input.params.mode === "digest"
|
||||
? "recent digests"
|
||||
: undefined)
|
||||
return memory.decide({
|
||||
root: input.root,
|
||||
decision: {
|
||||
kind: "recall",
|
||||
trigger: "targeted-recall",
|
||||
sessionID: input.current,
|
||||
result: input.hits.length ? "recalled" : "skipped",
|
||||
llm: false,
|
||||
parsed: false,
|
||||
fallback: false,
|
||||
reason: input.skipped,
|
||||
query,
|
||||
topics,
|
||||
files,
|
||||
tokens: MemoryToken.estimate(input.output),
|
||||
operationCount: input.hits.length,
|
||||
skippedCount: input.hits.length ? 0 : 1,
|
||||
summary: input.hits.length
|
||||
? `memory recall returned ${input.hits.length} ${input.params.mode} hits`
|
||||
: `memory recall found no ${input.params.mode} hits`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function miss(input: { params: RecallParams; current: string }) {
|
||||
const self = input.params.sessionID === input.current
|
||||
if (self && input.params.mode === "digest") {
|
||||
@@ -239,35 +194,6 @@ export namespace MemoryTool {
|
||||
})
|
||||
}
|
||||
|
||||
function catalogAudit(
|
||||
memory: MemoryService.Interface,
|
||||
input: {
|
||||
root: string
|
||||
current: string
|
||||
query: string
|
||||
result: { output: string; count: number; files: string[] }
|
||||
},
|
||||
) {
|
||||
return memory.decide({
|
||||
root: input.root,
|
||||
decision: {
|
||||
kind: "recall",
|
||||
trigger: "targeted-recall",
|
||||
sessionID: input.current,
|
||||
result: input.result.count ? "recalled" : "skipped",
|
||||
llm: false,
|
||||
parsed: false,
|
||||
fallback: false,
|
||||
query: input.query || "all keys",
|
||||
files: input.result.files,
|
||||
tokens: MemoryToken.estimate(input.result.output),
|
||||
operationCount: input.result.count,
|
||||
skippedCount: input.result.count ? 0 : 1,
|
||||
summary: `memory catalog listed ${input.result.count} entries`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function approvalRecall(input: Recall) {
|
||||
return input.ask({
|
||||
permission: "kilo_memory_recall",
|
||||
@@ -285,7 +211,6 @@ export namespace MemoryTool {
|
||||
return Effect.gen(function* () {
|
||||
const result = yield* catalog(input.memory, { root: live.root, query })
|
||||
const safe = { ...result, output: block(result.output) }
|
||||
yield* catalogAudit(input.memory, { root: live.root, current: live.current, query, result: safe })
|
||||
yield* input.memory.recordRecall({ root: live.root, sessionID: live.current, now: Date.now(), count: result.count })
|
||||
return {
|
||||
title: `Kilo memory catalog: ${result.count} entr${result.count === 1 ? "y" : "ies"}`,
|
||||
@@ -296,22 +221,13 @@ export namespace MemoryTool {
|
||||
}
|
||||
|
||||
function recallQuery(input: Recall, live: Live) {
|
||||
return Effect.gen(function* () {
|
||||
const output = "Provide a topic query for typed/search memory recall."
|
||||
yield* audit(input.memory, {
|
||||
root: live.root,
|
||||
params: input.params,
|
||||
current: live.current,
|
||||
hits: [],
|
||||
skipped: "missing_query",
|
||||
output,
|
||||
})
|
||||
return {
|
||||
return Effect.succeed(
|
||||
{
|
||||
title: `Kilo memory ${input.params.mode}: no query`,
|
||||
output,
|
||||
output: "Provide a topic query for typed/search memory recall.",
|
||||
metadata: { sources: [], count: 0 },
|
||||
} satisfies Result
|
||||
})
|
||||
} satisfies Result,
|
||||
)
|
||||
}
|
||||
|
||||
function recallSearch(input: Recall, live: Live, query: string, mode: MemoryRecall.Mode) {
|
||||
@@ -327,15 +243,7 @@ export namespace MemoryTool {
|
||||
limit,
|
||||
})
|
||||
const hits = result?.hits ?? []
|
||||
const self = input.params.sessionID === live.current
|
||||
const skipped =
|
||||
input.params.sessionID && input.params.mode === "digest" && hits.length === 0
|
||||
? self
|
||||
? "current_session_digest"
|
||||
: "missing_session_digest"
|
||||
: undefined
|
||||
const output = hits.length ? result!.block : miss({ params: input.params, current: live.current })
|
||||
yield* audit(input.memory, { root: live.root, params: input.params, current: live.current, hits, skipped, output })
|
||||
yield* input.memory.recordRecall({ root: live.root, sessionID: live.current, now: Date.now(), count: hits.length })
|
||||
|
||||
if (hits.length === 0) {
|
||||
@@ -440,29 +348,8 @@ export namespace MemoryTool {
|
||||
return saved(input)
|
||||
}
|
||||
|
||||
function skip(input: Base & { params: SaveParams }, root: string) {
|
||||
return Effect.gen(function* () {
|
||||
const reason = input.params.reason ?? "out_of_scope"
|
||||
yield* input.memory.decide({
|
||||
root,
|
||||
decision: {
|
||||
kind: "typed",
|
||||
trigger: "explicit",
|
||||
sessionID: input.sessionID,
|
||||
result: "skipped",
|
||||
llm: false,
|
||||
parsed: true,
|
||||
fallback: false,
|
||||
reason,
|
||||
tokens: 0,
|
||||
operationCount: 0,
|
||||
skippedCount: 1,
|
||||
skipped: [{ reason }],
|
||||
summary: `explicit memory save skipped: ${reason}`,
|
||||
},
|
||||
})
|
||||
return skipped({ reason })
|
||||
})
|
||||
function skip(input: Base & { params: SaveParams }) {
|
||||
return Effect.succeed(skipped({ reason: input.params.reason ?? "out_of_scope" }))
|
||||
}
|
||||
|
||||
function forget(input: Save, root: string) {
|
||||
@@ -497,7 +384,7 @@ export namespace MemoryTool {
|
||||
const state = yield* input.memory.state({ root })
|
||||
if (!state.enabled) return disabled()
|
||||
if (input.params.action === "forget") return yield* forget(input, root)
|
||||
if (input.params.action === "skip") return yield* skip(input, root)
|
||||
if (input.params.action === "skip") return yield* skip(input)
|
||||
return yield* write(input, root)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
auditOps,
|
||||
capturePlan,
|
||||
duplicateOps,
|
||||
errorReason,
|
||||
fallbackDigest,
|
||||
guardReason,
|
||||
hasSubstantialDiff,
|
||||
@@ -128,7 +128,7 @@ describe("memory capture parsing", () => {
|
||||
expect(() => salvageTyped(`{"op":"upsert_project_fact","key":"good_one","value":"Keep this fact."}`)).toThrow()
|
||||
})
|
||||
|
||||
test("redacts secrets in salvaged unsupported ops before they reach the audit", () => {
|
||||
test("redacts secrets in salvaged unsupported ops before they reach callers", () => {
|
||||
const parsed = salvageTyped(
|
||||
`{"operations":[{"op":"not_a_real_op","key":"leak","value":"key is sk-abcdefghijklmnopqrstuvwxyz"}],"skipped":[]}`,
|
||||
)
|
||||
@@ -169,17 +169,6 @@ describe("memory capture parsing", () => {
|
||||
expect(JSON.stringify(parsed.skipped)).not.toContain(secret.slice(0, 20))
|
||||
})
|
||||
|
||||
test("redacts remove audit queries before truncating", () => {
|
||||
const secret = "sk-" + "a".repeat(40)
|
||||
const query = "x".repeat(100) + secret
|
||||
const audit = auditOps([{ action: "remove", query }])
|
||||
const text = JSON.stringify(audit)
|
||||
|
||||
expect(text).toContain("[redacted]")
|
||||
expect(text).not.toContain(secret)
|
||||
expect(text).not.toContain(secret.slice(0, 20))
|
||||
})
|
||||
|
||||
test("truncates typed batches beyond the op cap instead of failing", () => {
|
||||
const ops = Array.from(
|
||||
{ length: 20 },
|
||||
@@ -332,6 +321,11 @@ describe("memory capture parsing", () => {
|
||||
input: base,
|
||||
expected: { session: true, digestDue: true, typedCall: true, typedWork: true, skipReason: undefined },
|
||||
},
|
||||
{
|
||||
name: "expected work: a persisted null typed clock does not throttle capture",
|
||||
input: { ...base, lastTypedConsolidationAt: null },
|
||||
expected: { session: true, digestDue: true, typedCall: true, typedWork: true, skipReason: undefined },
|
||||
},
|
||||
{
|
||||
name: "expected idle flush: completed turn inside interval skips now",
|
||||
input: { ...base, priorTime: 900, lastTypedConsolidationAt: 900 },
|
||||
@@ -650,6 +644,70 @@ describe("memory capture parsing", () => {
|
||||
)
|
||||
expect(guardReason("429 too many requests")).toBe("rate_limit_guard")
|
||||
expect(guardReason("billing credits exhausted")).toBe("quota_guard")
|
||||
expect(guardReason("request timed out")).toBe("transient")
|
||||
expect(guardReason("deadline exceeded")).toBe("transient")
|
||||
expect(guardReason("DeadlineExceeded")).toBe("transient")
|
||||
expect(guardReason('cause={"code":"ETIMEDOUT"}')).toBe("transient")
|
||||
expect(guardReason("504 Gateway Timeout")).toBe("transient")
|
||||
expect(guardReason("Connect Timeout Error")).toBe("transient")
|
||||
expect(guardReason("Headers Timeout Error")).toBe("transient")
|
||||
expect(guardReason("Body Timeout Error")).toBe("transient")
|
||||
expect(guardReason("connect_timeout")).toBe("transient")
|
||||
expect(guardReason('status=400 body={"error":"invalid parameter: timeout"}')).toBeUndefined()
|
||||
expect(guardReason("set request timeout to 30000")).toBeUndefined()
|
||||
const timeout = new Error("request aborted")
|
||||
timeout.name = "TimeoutError"
|
||||
expect(guardReason(timeout)).toBe("transient")
|
||||
expect(errorReason(timeout)).toBe("request aborted")
|
||||
expect(guardReason(Object.assign(new Error("request failed"), { status: 504 }))).toBe("transient")
|
||||
expect(
|
||||
guardReason(Object.assign(new Error("request failed"), { cause: { code: "UND_ERR_CONNECT_TIMEOUT" } })),
|
||||
).toBe("transient")
|
||||
expect(
|
||||
guardReason(Object.assign(new Error("request failed"), { cause: { code: "UND_ERR_HEADERS_TIMEOUT" } })),
|
||||
).toBe("transient")
|
||||
expect(guardReason(Object.assign(new Error("request failed"), { cause: { code: "ETIMEDOUT" } }))).toBe(
|
||||
"transient",
|
||||
)
|
||||
expect(
|
||||
guardReason(Object.assign(new Error("failed after 2 attempts"), { errors: [{ statusCode: 504 }] })),
|
||||
).toBe("transient")
|
||||
expect(guardReason(new Error("set request timeout to 30000"))).toBeUndefined()
|
||||
expect(guardReason(new Error("request timed out"))).toBe("transient")
|
||||
expect(guardReason(Object.assign(new Error("request failed"), { cause: new Error("connect timeout") }))).toBe(
|
||||
"transient",
|
||||
)
|
||||
expect(
|
||||
guardReason(
|
||||
Object.assign(new Error("failed after 2 attempts"), {
|
||||
errors: [Object.assign(new Error("rate limit reached"), { status: 429 }), timeout],
|
||||
}),
|
||||
),
|
||||
).toBe("rate_limit_guard")
|
||||
expect(
|
||||
guardReason(
|
||||
Object.assign(new Error("failed after 2 attempts"), {
|
||||
errors: [new Error("insufficient quota available"), timeout],
|
||||
}),
|
||||
),
|
||||
).toBe("quota_guard")
|
||||
expect(
|
||||
guardReason(
|
||||
Object.assign(new Error("request failed"), {
|
||||
cause: Object.assign(new Error("rate limit reached"), { status: 429 }),
|
||||
}),
|
||||
),
|
||||
).toBe("rate_limit_guard")
|
||||
expect(
|
||||
guardReason(
|
||||
Object.assign(new Error("timed out"), {
|
||||
name: "TimeoutError",
|
||||
cause: new Error("billing credit balance exhausted"),
|
||||
}),
|
||||
),
|
||||
).toBe("quota_guard")
|
||||
expect(guardReason("timeout after 429 too many requests")).toBe("rate_limit_guard")
|
||||
expect(guardReason("timeout after quota exceeded")).toBe("quota_guard")
|
||||
})
|
||||
|
||||
test("redacts common secret token shapes", () => {
|
||||
|
||||
@@ -50,6 +50,22 @@ describe("memory core package", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("legacy audit compatibility APIs remain no-op", async () => {
|
||||
await use(async (t) => {
|
||||
await Memory.enable({ root: t.root })
|
||||
await MemoryFiles.append(t.root, "provider error with sensitive detail")
|
||||
await MemoryFiles.decide(t.root, {
|
||||
kind: "typed",
|
||||
result: "error",
|
||||
reason: "provider error with sensitive detail",
|
||||
})
|
||||
|
||||
expect(await MemoryFiles.readChanges(t.root)).toBe("")
|
||||
expect(await MemoryFiles.readDecisions(t.root)).toBe("")
|
||||
expect(await Bun.file(path.join(t.root, "decisions.jsonl")).exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test("prepare removes legacy decisions once from owned memory roots", async () => {
|
||||
await use(async (t) => {
|
||||
await Memory.enable({ root: t.root })
|
||||
|
||||
@@ -7,6 +7,7 @@ import { digestPrompt, typedPrompt } from "../src/capture/capture"
|
||||
import { MemoryCapture } from "../src/effect/capture"
|
||||
import { MemoryEvents } from "../src/effect/events"
|
||||
import { KiloMemory } from "../src/effect/index"
|
||||
import { MemoryLog } from "../src/effect/log"
|
||||
import type { MemoryPorts } from "../src/effect/ports"
|
||||
import { MemoryService } from "../src/effect/service"
|
||||
import { MemoryTimers } from "../src/effect/timers"
|
||||
@@ -49,11 +50,18 @@ function session(turn: MemoryPorts.TurnView | undefined): MemoryPorts.SessionPor
|
||||
|
||||
/** Model port that answers digest/typed calls from canned JSON, keyed by system prompt so it is
|
||||
* order-independent (digest and typed run concurrently). */
|
||||
function model(input: { digest: string; typed: string; fallback?: string; onRun?: (system: string) => void }): MemoryPorts.ModelPort {
|
||||
function model(input: {
|
||||
digest: string
|
||||
typed: string
|
||||
fallback?: string
|
||||
fail?: Error
|
||||
onRun?: (system: string) => void
|
||||
}): MemoryPorts.ModelPort {
|
||||
return {
|
||||
resolve: () => Effect.succeed({ handle: {}, ...(input.fallback ? { fallback: { reason: input.fallback } } : {}) }),
|
||||
run: async ({ system }) => {
|
||||
input.onRun?.(system)
|
||||
if (system === typedPrompt && input.fail) throw input.fail
|
||||
const text = system === digestPrompt ? input.digest : system === typedPrompt ? input.typed : "{}"
|
||||
return { text, usage: USAGE }
|
||||
},
|
||||
@@ -66,6 +74,7 @@ function run(input: {
|
||||
model: MemoryPorts.ModelPort
|
||||
memoryModel?: string
|
||||
reason?: "completed" | "interrupted" | "error"
|
||||
bypassInterval?: boolean
|
||||
}) {
|
||||
return Effect.runPromise(
|
||||
MemoryCapture.turn({
|
||||
@@ -75,6 +84,7 @@ function run(input: {
|
||||
model: input.model,
|
||||
memoryModel: input.memoryModel,
|
||||
reason: input.reason ?? "completed",
|
||||
bypassInterval: input.bypassInterval,
|
||||
}).pipe(Effect.provideService(MemoryService.Service, MemoryService.make())),
|
||||
)
|
||||
}
|
||||
@@ -107,6 +117,121 @@ describe("MemoryCapture (fake ports)", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("typed timeout preserves digest progress without advancing the typed clock", async () => {
|
||||
const t = await tmp()
|
||||
const events: MemoryEvents.Status[] = []
|
||||
const logs: string[] = []
|
||||
try {
|
||||
await KiloMemory.enable({ root: t.root })
|
||||
await KiloMemory.configure({ root: t.root, settings: { autoConsolidate: true } })
|
||||
MemoryLog.setWarn((message, meta) => logs.push(`${message}:${meta?.reason}:${meta?.detail}`))
|
||||
MemoryEvents.setSink((input) => {
|
||||
events.push(input.payload)
|
||||
})
|
||||
|
||||
const result = await run({
|
||||
root: t.root,
|
||||
session: session(view()),
|
||||
model: model({
|
||||
digest: '{"topic":"repo setup","summary":"Explored repo setup commands. Next step: verify memory tests."}',
|
||||
typed: "{}",
|
||||
fail: new DOMException("memory model timed out", "TimeoutError"),
|
||||
}),
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ skipped: false, operationCount: 0 })
|
||||
const state = await MemoryFiles.readState(t.root)
|
||||
expect(state.stats.lastTypedConsolidationAt).toBeNull()
|
||||
expect(state.stats.lastSessionSavedAt).toEqual(expect.any(Number))
|
||||
expect(state.stats.lastConsolidatedMessageID).toBe("msg_assistant")
|
||||
expect(events.find((item) => item.state === "error")?.reason).toBe("transient")
|
||||
expect(logs).toEqual(["memory capture transient failure:transient:memory model timed out"])
|
||||
|
||||
const calls: string[] = []
|
||||
const retry = await run({
|
||||
root: t.root,
|
||||
session: session(view()),
|
||||
model: model({
|
||||
digest: "{}",
|
||||
typed: '{"operations":[],"skipped":[]}',
|
||||
onRun: (system) => calls.push(system),
|
||||
}),
|
||||
bypassInterval: true,
|
||||
})
|
||||
expect(retry).toMatchObject({ skipped: true, reason: "no_new_content" })
|
||||
expect(calls).toEqual([])
|
||||
|
||||
const next = await run({
|
||||
root: t.root,
|
||||
session: session(
|
||||
view({
|
||||
assistant: "Use bun install, then run the package tests and typecheck.",
|
||||
lastAssistantID: "msg_assistant_next",
|
||||
}),
|
||||
),
|
||||
model: model({
|
||||
digest: "{}",
|
||||
typed: '{"operations":[],"skipped":[]}',
|
||||
onRun: (system) => calls.push(system),
|
||||
}),
|
||||
})
|
||||
expect(next).toMatchObject({ skipped: false, operationCount: 0 })
|
||||
expect(calls).toEqual([typedPrompt])
|
||||
const updated = await MemoryFiles.readState(t.root)
|
||||
expect(updated.stats.lastTypedConsolidationAt).toEqual(expect.any(Number))
|
||||
expect(updated.stats.lastConsolidatedMessageID).toBe("msg_assistant_next")
|
||||
} finally {
|
||||
MemoryLog.setWarn(() => {})
|
||||
MemoryEvents.setSink(() => {})
|
||||
await t.done()
|
||||
}
|
||||
})
|
||||
|
||||
test("fallback commit preserves metrics from the prior successful typed consolidation", async () => {
|
||||
const t = await tmp()
|
||||
try {
|
||||
await KiloMemory.enable({ root: t.root })
|
||||
await KiloMemory.configure({ root: t.root, settings: { autoConsolidate: true } })
|
||||
|
||||
const first = await run({
|
||||
root: t.root,
|
||||
session: session(view()),
|
||||
model: model({
|
||||
digest: '{"topic":"repo setup","summary":"Explored repo setup."}',
|
||||
typed:
|
||||
'{"operations":[{"op":"upsert_environment_fact","section":"Commands","key":"test_cmd","value":"bun test"}],"skipped":[]}',
|
||||
}),
|
||||
})
|
||||
expect(first).toMatchObject({ skipped: false, operationCount: 1 })
|
||||
const initial = await MemoryFiles.readState(t.root)
|
||||
expect(initial.stats.lastOperationCount).toBe(1)
|
||||
expect(initial.stats.lastTypedConsolidationAt).toEqual(expect.any(Number))
|
||||
|
||||
const second = await run({
|
||||
root: t.root,
|
||||
session: session(
|
||||
view({
|
||||
assistant: "Investigated a timeout edge case.",
|
||||
lastAssistantID: "msg_assistant_timeout",
|
||||
}),
|
||||
),
|
||||
model: model({
|
||||
digest: '{"topic":"investigation","summary":"Investigated timeouts."}',
|
||||
typed: "{}",
|
||||
fail: new DOMException("memory model timed out", "TimeoutError"),
|
||||
}),
|
||||
bypassInterval: true,
|
||||
})
|
||||
expect(second).toMatchObject({ skipped: false, operationCount: 0 })
|
||||
const preserved = await MemoryFiles.readState(t.root)
|
||||
expect(preserved.stats.lastOperationCount).toBe(1)
|
||||
expect(preserved.stats.lastTypedConsolidationAt).toBe(initial.stats.lastTypedConsolidationAt)
|
||||
expect(preserved.stats.lastConsolidatedMessageID).toBe("msg_assistant_timeout")
|
||||
} finally {
|
||||
await t.done()
|
||||
}
|
||||
})
|
||||
|
||||
test("turn-close skips a secret-like op and applies the rest of the batch", async () => {
|
||||
const t = await tmp()
|
||||
const events: MemoryEvents.Status[] = []
|
||||
|
||||
@@ -508,8 +508,8 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
|
||||
}
|
||||
|
||||
/* "why was this allowed" line inside a tool's expanded body. Styled like
|
||||
[data-component="tool-hint"] (muted, italic) so it reads as ambient
|
||||
context rather than a call to action, and recedes the way reasoning text does. */
|
||||
[data-component="tool-hint"] (muted) so it reads as ambient context rather
|
||||
than a call to action, and recedes the way reasoning text does. */
|
||||
[data-slot="tool-approval-line"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -518,7 +518,6 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
|
||||
padding: 4px 0 6px;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: italic;
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--text-weak);
|
||||
opacity: 0.9;
|
||||
@@ -528,6 +527,11 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="tool-approval-decision"] {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
[data-slot="tool-approval-rule"] {
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ import { checksum } from "@opencode-ai/core/util/encode"
|
||||
import { Tooltip } from "./tooltip"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
|
||||
import { ToolApprovalProvider, resolveToolApproval } from "./tool-approval"
|
||||
import { ToolApprovalProvider, resolveToolApproval, useToolApproval } from "./tool-approval"
|
||||
export { ToolApprovalProvider, resolveToolApproval, ToolApprovalVisibilityProvider } from "./tool-approval"
|
||||
import { GrowBox } from "./grow-box"
|
||||
import { COLLAPSIBLE_SPRING } from "./motion"
|
||||
@@ -1888,10 +1888,13 @@ ToolRegistry.register({
|
||||
const pending = createMemo(() => busy(props.status))
|
||||
const images = createMemo(() => (props.attachments ?? []).filter((f) => f.mime.startsWith("image/") && f.url))
|
||||
const preview = (url: string, alt?: string) => dialog.show(() => <ImagePreview src={url} alt={alt} />)
|
||||
// Read is high-frequency and low-risk, so details stay hidden unless the target was outside
|
||||
// the workspace, in which case the approval reason explains what looks like an "agent escape".
|
||||
const approval = useToolApproval()
|
||||
return (
|
||||
<>
|
||||
<BasicTool
|
||||
hideDetails
|
||||
hideDetails={!approval()?.approval.outsideWorkspace}
|
||||
{...props}
|
||||
icon="glasses"
|
||||
onSubtitleClick={
|
||||
|
||||
@@ -54,4 +54,28 @@ describe("resolveToolApproval", () => {
|
||||
expect(out?.source).toBe("ui.approval.source.agent(agent=code)")
|
||||
expect(out?.rule).toBeUndefined()
|
||||
})
|
||||
|
||||
test("adds the outsideWorkspace text with just the filename when a path is known", () => {
|
||||
const approval = {
|
||||
source: "agent" as const,
|
||||
agent: "code",
|
||||
outsideWorkspace: true,
|
||||
outsideWorkspacePath: "/etc/secrets/hello.txt",
|
||||
}
|
||||
const out = resolveToolApproval({ approval }, t)
|
||||
expect(out?.outsideWorkspace).toBe("ui.approval.outsideWorkspace(file=hello.txt)")
|
||||
})
|
||||
|
||||
test("omits the outsideWorkspace text for an ordinary in-workspace approval", () => {
|
||||
const approval = { source: "agent" as const, agent: "code" }
|
||||
const out = resolveToolApproval({ approval }, t)
|
||||
expect(out?.outsideWorkspace).toBeUndefined()
|
||||
})
|
||||
|
||||
test("omits the outsideWorkspace text when outsideWorkspace is set but no path is known", () => {
|
||||
// e.g. a bash command scanning multiple external directories has no single filepath to show.
|
||||
const approval = { source: "agent" as const, agent: "code", outsideWorkspace: true }
|
||||
const out = resolveToolApproval({ approval }, t)
|
||||
expect(out?.outsideWorkspace).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createContext, useContext, Show, type Accessor, type ParentProps } from "solid-js"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { Icon } from "./icon"
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,10 @@ export type ToolApproval = {
|
||||
source: "agent" | "global" | "project" | "yolo" | "session" | "manual" | "default"
|
||||
agent?: string
|
||||
rule?: { permission: string; pattern: string; action: string }
|
||||
/** True when the tool call's target path was outside the workspace/worktree. */
|
||||
outsideWorkspace?: boolean
|
||||
/** The target file path, when known, for display as a filename next to the note above. */
|
||||
outsideWorkspacePath?: string
|
||||
}
|
||||
|
||||
/** Pre-resolved, localized text plus the raw approval, supplied by the caller. */
|
||||
@@ -20,6 +25,7 @@ export type ToolApprovalDisplay = {
|
||||
decision: string
|
||||
source?: string
|
||||
rule?: string
|
||||
outsideWorkspace?: string
|
||||
}
|
||||
|
||||
const SOURCE_KEYS = ["agent", "global", "project", "yolo", "session", "manual", "default"] as const
|
||||
@@ -82,11 +88,16 @@ export function resolveToolApproval(
|
||||
rule && !(rule.permission === "*" && rule.pattern === "*")
|
||||
? t("ui.approval.rule", { permission: rule.permission, pattern: rule.pattern })
|
||||
: undefined
|
||||
// Only worth calling out when we know which file it was; a bare "outside your workspace" note
|
||||
// without a filename (e.g. a bash command touching several directories) isn't actionable.
|
||||
const filename = approval.outsideWorkspacePath ? getFilename(approval.outsideWorkspacePath) : undefined
|
||||
return {
|
||||
approval,
|
||||
decision: approval.source === "manual" ? t("ui.approval.manual") : t("ui.approval.auto"),
|
||||
source: sourceText(),
|
||||
rule: ruleText,
|
||||
outsideWorkspace:
|
||||
approval.outsideWorkspace && filename ? t("ui.approval.outsideWorkspace", { file: filename }) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +112,9 @@ export function ToolApprovalLine(props: { display: ToolApprovalDisplay }) {
|
||||
<Show when={props.display.source}>{(text) => <span data-slot="tool-approval-source">{text()}</span>}</Show>
|
||||
<Show when={props.display.rule}>{(text) => <span data-slot="tool-approval-rule">{text()}</span>}</Show>
|
||||
</Show>
|
||||
<Show when={props.display.outsideWorkspace}>
|
||||
{(text) => <span data-slot="tool-approval-outside-workspace">{text()}</span>}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as path from "path"
|
||||
import { existsSync } from "fs"
|
||||
import * as vscode from "vscode"
|
||||
import { TRANSIENT as MEMORY_TRANSIENT } from "@kilocode/kilo-memory/schema"
|
||||
import type {
|
||||
KiloClient,
|
||||
Session,
|
||||
@@ -4307,8 +4308,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
for (const sid of trackedByDir) targets.add(sid)
|
||||
if (local && active) targets.add(active)
|
||||
if (targets.size === 0 && local) targets.add(undefined)
|
||||
const detail =
|
||||
props.detail && typeof props.detail === "object"
|
||||
const transient = event.type === "memory.error" && props.reason === MEMORY_TRANSIENT
|
||||
const detail = transient
|
||||
? undefined
|
||||
: props.detail && typeof props.detail === "object"
|
||||
? props.detail
|
||||
: event.type === "memory.error" && typeof props.reason === "string"
|
||||
? { type: "error", message: props.reason, reason: props.reason }
|
||||
|
||||
@@ -73,6 +73,7 @@ function slimEdit(state: Record<string, unknown>): Record<string, unknown> {
|
||||
}
|
||||
}
|
||||
if (meta.diagnostics) result.diagnostics = meta.diagnostics
|
||||
if (meta.approval) result.approval = meta.approval
|
||||
next.metadata = result
|
||||
return next
|
||||
}
|
||||
@@ -84,6 +85,7 @@ function slimPatch(state: Record<string, unknown>): Record<string, unknown> {
|
||||
if (isObj(meta)) {
|
||||
const slim: Record<string, unknown> = {}
|
||||
if (meta.diagnostics) slim.diagnostics = meta.diagnostics
|
||||
if (meta.approval) slim.approval = meta.approval
|
||||
if (Array.isArray(meta.files)) {
|
||||
slim.files = (meta.files as Record<string, unknown>[]).map((f) => {
|
||||
const diff = patch(f.patch) ?? patch(f.diff)
|
||||
@@ -115,6 +117,7 @@ function slimMultiedit(state: Record<string, unknown>): Record<string, unknown>
|
||||
if (isObj(meta)) {
|
||||
const slim: Record<string, unknown> = {}
|
||||
if (meta.diagnostics) slim.diagnostics = meta.diagnostics
|
||||
if (meta.approval) slim.approval = meta.approval
|
||||
if (Array.isArray(meta.results)) {
|
||||
slim.results = (meta.results as Record<string, unknown>[]).map((r) => {
|
||||
const rs: Record<string, unknown> = {}
|
||||
@@ -149,6 +152,7 @@ function slimWrite(state: Record<string, unknown>): Record<string, unknown> {
|
||||
if (meta.filepath) slim.filepath = meta.filepath
|
||||
if (meta.exists !== undefined) slim.exists = meta.exists
|
||||
if (meta.diagnostics) slim.diagnostics = meta.diagnostics
|
||||
if (meta.approval) slim.approval = meta.approval
|
||||
const fd = meta.filediff
|
||||
if (isObj(fd)) {
|
||||
slim.filediff = {
|
||||
|
||||
@@ -12,14 +12,7 @@ export const EnvSchema = z
|
||||
.trim()
|
||||
.regex(/^[A-Z_][A-Z0-9_]*$/, INVALID_ENV)
|
||||
|
||||
const VariantConfigSchema = z.object({
|
||||
enable_thinking: z.boolean().optional(),
|
||||
thinking: z.object({ type: z.enum(["enabled", "disabled", "adaptive"]) }).optional(),
|
||||
reasoning_split: z.boolean().optional(),
|
||||
reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(),
|
||||
effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
|
||||
chat_template_args: z.object({ enable_thinking: z.boolean() }).optional(),
|
||||
})
|
||||
const VariantConfigSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
export type VariantConfig = z.infer<typeof VariantConfigSchema>
|
||||
|
||||
|
||||
@@ -80,6 +80,20 @@ test("auto efficient details show server description and model choices", async (
|
||||
await expect(preview).not.toContainText("openai/gpt-5.5")
|
||||
})
|
||||
|
||||
test("auto frontier details show model choices when routes are present", async ({ page }) => {
|
||||
await load(page, "shared--model-selector-accessible")
|
||||
|
||||
await page.getByRole("button", { name: "Review model: Alpha" }).click()
|
||||
await page.getByRole("treeitem", { name: /Kilo Auto Frontier/ }).click()
|
||||
|
||||
const preview = page.locator(".model-selector-preview")
|
||||
await expect(preview).toContainText("Routes each request to the strongest available models.")
|
||||
await expect(preview).toContainText("Model choices")
|
||||
await expect(preview).toContainText("openai/gpt-5.5")
|
||||
await expect(preview).toContainText("anthropic/claude-opus-4.6")
|
||||
await expect(preview).not.toContainText("google/gemini-2.5-flash")
|
||||
})
|
||||
|
||||
test("search uses a flat relevance-ranked result list with provider labels", async ({ page }) => {
|
||||
await load(page, "shared--model-selector-accessible")
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ describe("validateCustomProvider – variant name validation", () => {
|
||||
]
|
||||
const out = validateCustomProvider(args(form))
|
||||
expect(out.result).toBeUndefined()
|
||||
expect(out.errors.models[0].variants?.[0]?.name).toBe("provider.custom.error.required")
|
||||
expect(out.errors.models[0].variants?.[0]?.name).toBe('variants[""]: provider.custom.error.required')
|
||||
})
|
||||
|
||||
it("blocks submit and reports error when reasoning is enabled with a whitespace-only variant name", () => {
|
||||
@@ -112,7 +112,7 @@ describe("validateCustomProvider – variant name validation", () => {
|
||||
]
|
||||
const out = validateCustomProvider(args(form))
|
||||
expect(out.result).toBeUndefined()
|
||||
expect(out.errors.models[0].variants?.[0]?.name).toBe("provider.custom.error.required")
|
||||
expect(out.errors.models[0].variants?.[0]?.name).toBe('variants[" "]: provider.custom.error.required')
|
||||
})
|
||||
|
||||
it("blocks submit and reports duplicate error for two variants with the same name", () => {
|
||||
@@ -140,7 +140,7 @@ describe("validateCustomProvider – variant name validation", () => {
|
||||
]
|
||||
const out = validateCustomProvider(args(form))
|
||||
expect(out.result).toBeUndefined()
|
||||
expect(out.errors.models[0].variants?.[1]?.name).toBe("provider.custom.error.duplicate")
|
||||
expect(out.errors.models[0].variants?.[1]?.name).toBe('variants["fast"]: provider.custom.error.duplicate')
|
||||
})
|
||||
|
||||
it("ignores variants entirely when reasoning is disabled, even if they have empty names", () => {
|
||||
@@ -206,6 +206,34 @@ describe("validateCustomProvider – variant name validation", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves opaque variant options after the editor controls are removed", () => {
|
||||
const form = base()
|
||||
const raw = {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
customOption: { enabled: true },
|
||||
}
|
||||
form.models[0].reasoning = true
|
||||
form.models[0].variants = [
|
||||
{
|
||||
name: "high",
|
||||
raw,
|
||||
enableThinking: undefined,
|
||||
thinking: "adaptive",
|
||||
splitReasoning: undefined,
|
||||
outputEffort: undefined,
|
||||
reasoningEffort: undefined,
|
||||
chatTemplateArgs: undefined,
|
||||
},
|
||||
]
|
||||
|
||||
const out = validateCustomProvider(args(form))
|
||||
expect(out.result).toBeDefined()
|
||||
const saved = out.result!.config.models["model-1"] as Record<string, unknown>
|
||||
expect(saved.variants).toEqual({ high: raw })
|
||||
})
|
||||
|
||||
it("serializes image modality when supportsImages is set", () => {
|
||||
const form = base()
|
||||
form.models[0].supportsImages = true
|
||||
|
||||
@@ -162,6 +162,35 @@ describe("sanitizeCustomProviderConfig", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves opaque options on existing variants", () => {
|
||||
const variant = {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
reasoningEffort: "max",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
customOption: { enabled: true },
|
||||
}
|
||||
const result = sanitizeCustomProviderConfig({
|
||||
name: "Thinking Provider",
|
||||
options: { baseURL: "https://example.com/v1" },
|
||||
models: {
|
||||
"model-1": {
|
||||
name: "Model One",
|
||||
variants: { high: variant },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
value: {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
name: "Thinking Provider",
|
||||
options: { baseURL: "https://example.com/v1" },
|
||||
models: { "model-1": { name: "Model One", variants: { high: variant } } },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves core custom model modalities", () => {
|
||||
const result = sanitizeCustomProviderConfig({
|
||||
name: "Media Provider",
|
||||
|
||||
@@ -127,6 +127,43 @@ describe("KiloProvider memory events", () => {
|
||||
expect(calls).toEqual(["/repo", "/repo"])
|
||||
})
|
||||
|
||||
it("refreshes status without forwarding transient memory errors", async () => {
|
||||
const calls: string[] = []
|
||||
const posts: unknown[] = []
|
||||
const client = {
|
||||
memory: {
|
||||
status: async (input: { directory: string }) => {
|
||||
calls.push(input.directory)
|
||||
return { data: status(input.directory) }
|
||||
},
|
||||
},
|
||||
} as unknown as KiloClient
|
||||
const provider = new KiloProvider(
|
||||
{} as never,
|
||||
{
|
||||
getClient: () => client,
|
||||
} as never,
|
||||
)
|
||||
const item = provider as unknown as Internals
|
||||
item.webview = { postMessage: async (message) => posts.push(message) }
|
||||
item.currentSession = { id: "ses_active" }
|
||||
item.trackedSessionIds.add("ses_active")
|
||||
provider.setSessionDirectory("ses_active", "/repo")
|
||||
|
||||
item.handleEvent(
|
||||
{
|
||||
type: "memory.error",
|
||||
properties: { sessionID: "ses_active", reason: "transient" },
|
||||
},
|
||||
"/repo",
|
||||
)
|
||||
await item.memory.idle()
|
||||
|
||||
expect(posts).not.toContainEqual(expect.objectContaining({ type: "memoryEvent" }))
|
||||
expect(posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", sessionID: "ses_active" }))
|
||||
expect(calls).toEqual(["/repo"])
|
||||
})
|
||||
|
||||
it("uses the project directory when toggling memory", async () => {
|
||||
const calls: unknown[] = []
|
||||
const client = {
|
||||
|
||||
@@ -18,6 +18,11 @@ describe("fmtPrice", () => {
|
||||
expect(fmtPrice(0)).toBe("Free")
|
||||
})
|
||||
|
||||
it("returns dash for negative price", () => {
|
||||
expect(fmtPrice(-1)).toBe("—")
|
||||
expect(fmtPrice(-1000000)).toBe("—")
|
||||
})
|
||||
|
||||
it("uses 4 decimal places for sub-cent prices", () => {
|
||||
expect(fmtPrice(0.005)).toBe("$0.0050/1M")
|
||||
})
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("isAuto", () => {
|
||||
})
|
||||
|
||||
describe("autoChoices", () => {
|
||||
it("uses backend Auto Efficient routes and resolves names when available", () => {
|
||||
it("uses backend Auto routes and resolves names when available", () => {
|
||||
expect(
|
||||
autoChoices(
|
||||
{
|
||||
@@ -144,12 +144,32 @@ describe("autoChoices", () => {
|
||||
])
|
||||
})
|
||||
|
||||
it("ignores missing routes and non-efficient Auto models", () => {
|
||||
it("shows routes for any Auto model when present", () => {
|
||||
expect(
|
||||
autoChoices(
|
||||
{
|
||||
providerID: KILO_GATEWAY_ID,
|
||||
id: "kilo-auto/frontier",
|
||||
autoRouting: { models: ["provider/model"] },
|
||||
},
|
||||
[{ id: "provider/model", name: "Provider: Model" }],
|
||||
),
|
||||
).toEqual([{ id: "provider/model", name: "Model" }])
|
||||
expect(
|
||||
autoChoices({
|
||||
providerID: KILO_GATEWAY_ID,
|
||||
id: "kilo-auto/free",
|
||||
autoRouting: { models: ["provider/model"] },
|
||||
}),
|
||||
).toEqual([{ id: "provider/model", name: "provider/model" }])
|
||||
})
|
||||
|
||||
it("ignores missing routes and non-Auto models", () => {
|
||||
expect(autoChoices({ providerID: KILO_GATEWAY_ID, id: "kilo-auto/efficient" })).toEqual([])
|
||||
expect(
|
||||
autoChoices({
|
||||
providerID: KILO_GATEWAY_ID,
|
||||
id: "kilo-auto/frontier",
|
||||
id: "anthropic/claude-sonnet",
|
||||
autoRouting: { models: ["provider/model"] },
|
||||
}),
|
||||
).toEqual([])
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { configMessage } from "../../webview-ui/src/utils/open-config"
|
||||
|
||||
describe("configMessage", () => {
|
||||
it("builds a global config request with localized labels", () => {
|
||||
const message = configMessage("global", (key, params) => `${key}:${params?.scope ?? ""}`)
|
||||
|
||||
expect(message.type).toBe("openConfigFile")
|
||||
expect(message.scope).toBe("global")
|
||||
expect(message.labels.scope).toBe("settings.config.scope.global:")
|
||||
expect(message.labels.title).toBe("settings.config.title:settings.config.scope.global:")
|
||||
expect(message.labels.openFailed).toBe("settings.config.openFailed:settings.config.scope.global:")
|
||||
})
|
||||
})
|
||||
@@ -206,6 +206,27 @@ describe("saveCustomProvider", () => {
|
||||
expect(calls.set).toEqual([{ providerID: "myprovider", auth: { type: "api", key: "sk-test" } }])
|
||||
})
|
||||
|
||||
it("preserves opaque existing variant options through the save boundary", async () => {
|
||||
const variant = {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
reasoningEffort: "custom",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
customOption: { enabled: true },
|
||||
}
|
||||
const saved = {
|
||||
...createSavedProvider(),
|
||||
models: { "model-1": { name: "Model One", reasoning: true, variants: { high: variant } } },
|
||||
}
|
||||
const existing = { disabled_providers: [], provider: { myprovider: saved } }
|
||||
const { ctx, calls, setCachedConfig } = createCtx(existing)
|
||||
|
||||
await saveCustomProvider(ctx, "req", "myprovider", saved, undefined, false, null, setCachedConfig)
|
||||
|
||||
const provider = (calls.config[0]?.config.provider as Record<string, typeof saved>).myprovider
|
||||
expect(provider.models["model-1"].variants.high).toEqual(variant)
|
||||
})
|
||||
|
||||
// Regression tests for https://github.com/Kilo-Org/kilocode/issues/9186
|
||||
//
|
||||
// The CLI's config.update endpoint deep-merges its payload with the existing
|
||||
|
||||
@@ -30,6 +30,10 @@ const BIG = "x".repeat(200_000) // 200 KB — typical file content size
|
||||
const DIAG = [
|
||||
{ range: { start: { line: 1, character: 0 }, end: { line: 1, character: 5 } }, message: "err", severity: 1 },
|
||||
]
|
||||
// Regression for #13001: slimmers used to rebuild `metadata` from an explicit allowlist that
|
||||
// didn't include `approval`, silently dropping the auto-approval reason (and the
|
||||
// outside-workspace note) before it ever reached the webview.
|
||||
const APPROVAL = { source: "agent", agent: "code", outsideWorkspace: true, outsideWorkspacePath: "/tmp/a.ts" }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -96,6 +100,7 @@ describe("slimPart", () => {
|
||||
diff: BIG,
|
||||
filediff: { file: "/a.ts", patch: PATCH, before: BIG, after: BIG, additions: 3, deletions: 1 },
|
||||
diagnostics: { "/a.ts": DIAG },
|
||||
approval: APPROVAL,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -103,7 +108,7 @@ describe("slimPart", () => {
|
||||
expect(bytes(slimPart(heavy))).toBeLessThan(MAX_SLIM_BYTES)
|
||||
})
|
||||
|
||||
it("keeps filediff counts and diagnostics", () => {
|
||||
it("keeps filediff counts, diagnostics, and approval", () => {
|
||||
const slim = slimPart(heavy) as Record<string, any>
|
||||
const meta = slim.state.metadata
|
||||
expect(meta.filediff.file).toBe("/a.ts")
|
||||
@@ -111,6 +116,7 @@ describe("slimPart", () => {
|
||||
expect(meta.filediff.additions).toBe(3)
|
||||
expect(meta.filediff.deletions).toBe(1)
|
||||
expect(meta.diagnostics).toEqual({ "/a.ts": DIAG })
|
||||
expect(meta.approval).toEqual(APPROVAL)
|
||||
})
|
||||
|
||||
it("keeps output and input intact", () => {
|
||||
@@ -176,6 +182,7 @@ describe("slimPart", () => {
|
||||
},
|
||||
],
|
||||
diagnostics: { "/a.ts": DIAG },
|
||||
approval: APPROVAL,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -183,7 +190,7 @@ describe("slimPart", () => {
|
||||
expect(bytes(slimPart(heavy))).toBeLessThan(MAX_SLIM_BYTES)
|
||||
})
|
||||
|
||||
it("keeps file summary fields and diagnostics", () => {
|
||||
it("keeps file summary fields, diagnostics, and approval", () => {
|
||||
const slim = slimPart(heavy) as Record<string, any>
|
||||
const meta = slim.state.metadata
|
||||
expect(meta.files[0].filePath).toBe("/a.ts")
|
||||
@@ -193,6 +200,7 @@ describe("slimPart", () => {
|
||||
expect(meta.files[0].additions).toBe(5)
|
||||
expect(meta.files[1].type).toBe("add")
|
||||
expect(meta.diagnostics).toEqual({ "/a.ts": DIAG })
|
||||
expect(meta.approval).toEqual(APPROVAL)
|
||||
})
|
||||
|
||||
it("drops unknown heavy metadata fields", () => {
|
||||
@@ -266,6 +274,7 @@ describe("slimPart", () => {
|
||||
},
|
||||
{ filediff: { file: "/b.ts", before: BIG, after: BIG, additions: 2, deletions: 0 }, diagnostics: {} },
|
||||
],
|
||||
approval: APPROVAL,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -273,7 +282,7 @@ describe("slimPart", () => {
|
||||
expect(bytes(slimPart(heavy))).toBeLessThan(MAX_SLIM_BYTES)
|
||||
})
|
||||
|
||||
it("keeps filediff counts and per-result diagnostics", () => {
|
||||
it("keeps filediff counts, per-result diagnostics, and approval", () => {
|
||||
const slim = slimPart(heavy) as Record<string, any>
|
||||
const meta = slim.state.metadata
|
||||
expect(meta.results[0].filediff.file).toBe("/a.ts")
|
||||
@@ -282,6 +291,7 @@ describe("slimPart", () => {
|
||||
expect(meta.results[0].diagnostics).toEqual({ "/a.ts": DIAG })
|
||||
expect(meta.results[1].filediff.file).toBe("/b.ts")
|
||||
expect(meta.diagnostics).toEqual({ "/a.ts": DIAG })
|
||||
expect(meta.approval).toEqual(APPROVAL)
|
||||
})
|
||||
|
||||
it("drops unknown heavy metadata fields", () => {
|
||||
@@ -318,6 +328,7 @@ describe("slimPart", () => {
|
||||
diff: BIG,
|
||||
filediff: { file: "/a.ts", patch: PATCH, before: BIG, after: BIG, additions: 100, deletions: 0 },
|
||||
diagnostics: { "/a.ts": DIAG },
|
||||
approval: APPROVAL,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -325,7 +336,7 @@ describe("slimPart", () => {
|
||||
expect(bytes(slimPart(heavy))).toBeLessThan(MAX_SLIM_BYTES)
|
||||
})
|
||||
|
||||
it("keeps filepath, exists, filediff counts, diagnostics", () => {
|
||||
it("keeps filepath, exists, filediff counts, diagnostics, and approval", () => {
|
||||
const slim = slimPart(heavy) as Record<string, any>
|
||||
const meta = slim.state.metadata
|
||||
expect(meta.filepath).toBe("/a.ts")
|
||||
@@ -335,6 +346,7 @@ describe("slimPart", () => {
|
||||
expect(meta.filediff.additions).toBe(100)
|
||||
expect(meta.filediff.deletions).toBe(0)
|
||||
expect(meta.diagnostics).toEqual({ "/a.ts": DIAG })
|
||||
expect(meta.approval).toEqual(APPROVAL)
|
||||
})
|
||||
|
||||
it("drops unknown heavy metadata fields", () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useProvider } from "../../context/provider"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
import type { ExtensionMessage, ProviderAuthState, ProviderConfig } from "../../types/messages"
|
||||
import { createProviderAction } from "../../utils/provider-action"
|
||||
import { configMessage } from "../../utils/open-config"
|
||||
import { MASKED_CUSTOM_PROVIDER_KEY, resolveCustomProviderKey } from "../../../../src/shared/custom-provider"
|
||||
import {
|
||||
CUSTOM_PROVIDER_PACKAGE,
|
||||
@@ -89,6 +90,7 @@ function modes(raw: unknown): Modalities {
|
||||
function parseVariant([name, cfg]: [string, Record<string, unknown>]): VariantEntry {
|
||||
return {
|
||||
name,
|
||||
raw: cfg,
|
||||
enableThinking: typeof cfg.enable_thinking === "boolean" ? cfg.enable_thinking : undefined,
|
||||
thinking:
|
||||
typeof cfg.thinking === "object" && cfg.thinking !== null
|
||||
@@ -460,25 +462,6 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
|
||||
setErrors("headers", (v) => v.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
function addVariant(mi: number) {
|
||||
const blank: VariantEntry = {
|
||||
name: "",
|
||||
enableThinking: undefined,
|
||||
thinking: undefined,
|
||||
splitReasoning: undefined,
|
||||
reasoningEffort: undefined,
|
||||
outputEffort: undefined,
|
||||
chatTemplateArgs: undefined,
|
||||
}
|
||||
setForm("models", mi, "variants", (v) => [...v, blank])
|
||||
setErrors("models", mi, "variants", (v) => [...(v ?? []), {}])
|
||||
}
|
||||
|
||||
function removeVariant(mi: number, vi: number) {
|
||||
setForm("models", mi, "variants", (v) => v.filter((_, i) => i !== vi))
|
||||
setErrors("models", mi, "variants", (v) => (v ?? []).filter((_, i) => i !== vi))
|
||||
}
|
||||
|
||||
function validate() {
|
||||
const output = validateCustomProvider({
|
||||
form,
|
||||
@@ -581,6 +564,19 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
|
||||
{language.t("provider.custom.description.link")}
|
||||
</a>
|
||||
{language.t("provider.custom.description.suffix")}
|
||||
<Show when={editing()}>
|
||||
<div style={{ "margin-top": "8px" }}>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
vscode.postMessage(configMessage("global", language.t))
|
||||
}}
|
||||
>
|
||||
{language.t("provider.custom.edit.advanced")}
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "16px" }}>
|
||||
@@ -673,7 +669,6 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
|
||||
{(m, i) => (
|
||||
<ModelCard
|
||||
m={m}
|
||||
i={i}
|
||||
errors={errors.models[i()] ?? {}}
|
||||
t={language.t}
|
||||
canRemove={form.models.length > 1}
|
||||
@@ -682,23 +677,6 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
|
||||
onChangeReasoning={(v) => setForm("models", i(), "reasoning", v)}
|
||||
onChangeSupportsImages={(v) => setForm("models", i(), "supportsImages", v)}
|
||||
onRemove={() => removeModel(i())}
|
||||
onAddVariant={() => addVariant(i())}
|
||||
onRemoveVariant={(vi) => removeVariant(i(), vi)}
|
||||
onChangeVariantName={(vi, val) => setForm("models", i(), "variants", vi, "name", val)}
|
||||
onChangeVariantEnableThinking={(vi, val) =>
|
||||
setForm("models", i(), "variants", vi, "enableThinking", val)
|
||||
}
|
||||
onChangeVariantThinking={(vi, val) => setForm("models", i(), "variants", vi, "thinking", val)}
|
||||
onChangeVariantSplitReasoning={(vi, val) =>
|
||||
setForm("models", i(), "variants", vi, "splitReasoning", val)
|
||||
}
|
||||
onChangeVariantReasoningEffort={(vi, val) =>
|
||||
setForm("models", i(), "variants", vi, "reasoningEffort", val)
|
||||
}
|
||||
onChangeVariantOutputEffort={(vi, val) => setForm("models", i(), "variants", vi, "outputEffort", val)}
|
||||
onChangeVariantChatTemplateArgs={(vi, val) =>
|
||||
setForm("models", i(), "variants", vi, "chatTemplateArgs", val)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
+13
-301
@@ -1,8 +1,6 @@
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Select } from "@kilocode/kilo-ui/select"
|
||||
import { TextField } from "@kilocode/kilo-ui/text-field"
|
||||
import { For, Show } from "solid-js"
|
||||
import { Show } from "solid-js"
|
||||
import { useLanguage } from "../../context/language"
|
||||
|
||||
export type Translator = ReturnType<typeof useLanguage>["t"]
|
||||
@@ -23,6 +21,7 @@ export type Modalities = {
|
||||
|
||||
export type VariantEntry = {
|
||||
name: string
|
||||
raw?: Record<string, unknown>
|
||||
enableThinking: EnableThinkingValue
|
||||
thinking: ThinkingTypeValue
|
||||
splitReasoning: SplitReasoningValue
|
||||
@@ -40,264 +39,8 @@ export type ModelEntry = {
|
||||
variants: VariantEntry[]
|
||||
}
|
||||
|
||||
type SelectOption<T> = { value: T; labelKey: string }
|
||||
|
||||
const ENABLE_THINKING_OPTIONS: SelectOption<EnableThinkingValue>[] = [
|
||||
{ value: undefined, labelKey: "provider.custom.models.variants.option.unset" },
|
||||
{ value: true, labelKey: "provider.custom.models.variants.enableThinking.true" },
|
||||
{ value: false, labelKey: "provider.custom.models.variants.enableThinking.false" },
|
||||
]
|
||||
|
||||
const THINKING_OPTIONS: SelectOption<ThinkingTypeValue>[] = [
|
||||
{ value: undefined, labelKey: "provider.custom.models.variants.option.unset" },
|
||||
{ value: "enabled", labelKey: "provider.custom.models.variants.thinking.enabled" },
|
||||
{ value: "disabled", labelKey: "provider.custom.models.variants.thinking.disabled" },
|
||||
{ value: "adaptive", labelKey: "provider.custom.models.variants.thinking.adaptive" },
|
||||
]
|
||||
|
||||
const SPLIT_REASONING_OPTIONS: SelectOption<SplitReasoningValue>[] = [
|
||||
{ value: undefined, labelKey: "provider.custom.models.variants.option.unset" },
|
||||
{ value: true, labelKey: "provider.custom.models.variants.splitReasoning.true" },
|
||||
{ value: false, labelKey: "provider.custom.models.variants.splitReasoning.false" },
|
||||
]
|
||||
|
||||
const CHAT_TEMPLATE_ARGS_OPTIONS: SelectOption<ChatTemplateArgsValue>[] = [
|
||||
{ value: undefined, labelKey: "provider.custom.models.variants.option.unset" },
|
||||
{ value: true, labelKey: "provider.custom.models.variants.chatTemplateArgs.true" },
|
||||
{ value: false, labelKey: "provider.custom.models.variants.chatTemplateArgs.false" },
|
||||
]
|
||||
|
||||
const REASONING_EFFORT_OPTIONS: SelectOption<ReasoningEffortValue>[] = [
|
||||
{ value: undefined, labelKey: "provider.custom.models.variants.option.unset" },
|
||||
{ value: "none", labelKey: "provider.custom.models.variants.reasoningEffort.none" },
|
||||
{ value: "minimal", labelKey: "provider.custom.models.variants.reasoningEffort.minimal" },
|
||||
{ value: "low", labelKey: "provider.custom.models.variants.reasoningEffort.low" },
|
||||
{ value: "medium", labelKey: "provider.custom.models.variants.reasoningEffort.medium" },
|
||||
{ value: "high", labelKey: "provider.custom.models.variants.reasoningEffort.high" },
|
||||
{ value: "xhigh", labelKey: "provider.custom.models.variants.reasoningEffort.xhigh" },
|
||||
]
|
||||
|
||||
const OUTPUT_EFFORT_OPTIONS: SelectOption<OutputEffortValue>[] = [
|
||||
{ value: undefined, labelKey: "provider.custom.models.variants.option.unset" },
|
||||
{ value: "low", labelKey: "provider.custom.models.variants.outputEffort.low" },
|
||||
{ value: "medium", labelKey: "provider.custom.models.variants.outputEffort.medium" },
|
||||
{ value: "high", labelKey: "provider.custom.models.variants.outputEffort.high" },
|
||||
{ value: "xhigh", labelKey: "provider.custom.models.variants.outputEffort.xhigh" },
|
||||
{ value: "max", labelKey: "provider.custom.models.variants.outputEffort.max" },
|
||||
]
|
||||
|
||||
type VariantRowProps = {
|
||||
v: VariantEntry
|
||||
vi: () => number
|
||||
isFirst: () => boolean
|
||||
error: { name?: string } | undefined
|
||||
t: Translator
|
||||
onChangeName: (val: string) => void
|
||||
onChangeEnableThinking: (val: EnableThinkingValue) => void
|
||||
onChangeThinking: (val: ThinkingTypeValue) => void
|
||||
onChangeSplitReasoning: (val: SplitReasoningValue) => void
|
||||
onChangeReasoningEffort: (val: ReasoningEffortValue) => void
|
||||
onChangeOutputEffort: (val: OutputEffortValue) => void
|
||||
onChangeChatTemplateArgs: (val: ChatTemplateArgsValue) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
function VariantRow(props: VariantRowProps) {
|
||||
return (
|
||||
<div>
|
||||
<Show when={!props.isFirst()}>
|
||||
<div
|
||||
style={{
|
||||
"border-top": "1px solid var(--border-weak-base, var(--vscode-panel-border))",
|
||||
margin: "4px 0",
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
"align-items": "stretch",
|
||||
"flex-direction": "column",
|
||||
"padding-top": "4px",
|
||||
}}
|
||||
>
|
||||
<div style={{ "min-width": "100px", flex: "1 1 80px" }}>
|
||||
<TextField
|
||||
label={props.t("provider.custom.models.variants.name.label")}
|
||||
placeholder={props.t("provider.custom.models.variants.name.placeholder")}
|
||||
value={props.v.name}
|
||||
onChange={props.onChangeName}
|
||||
validationState={props.error?.name ? "invalid" : undefined}
|
||||
error={props.error?.name}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "4px",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{ "font-size": "var(--kilo-font-size-12)", "font-weight": "500", color: "var(--text-weak-base)" }}
|
||||
>
|
||||
{props.t("provider.custom.models.variants.enableThinking.label")}
|
||||
</label>
|
||||
<Select
|
||||
options={ENABLE_THINKING_OPTIONS}
|
||||
current={ENABLE_THINKING_OPTIONS.find((o) => o.value === props.v.enableThinking)}
|
||||
value={(o) => String(o.value)}
|
||||
label={(o) => props.t(o.labelKey)}
|
||||
onSelect={(o) => props.onChangeEnableThinking(o?.value)}
|
||||
placeholder={props.t("provider.custom.models.variants.enableThinking.placeholder")}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "4px",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{ "font-size": "var(--kilo-font-size-12)", "font-weight": "500", color: "var(--text-weak-base)" }}
|
||||
>
|
||||
{props.t("provider.custom.models.variants.thinking.label")}
|
||||
</label>
|
||||
<Select
|
||||
options={THINKING_OPTIONS}
|
||||
current={THINKING_OPTIONS.find((o) => o.value === props.v.thinking)}
|
||||
value={(o) => String(o.value)}
|
||||
label={(o) => props.t(o.labelKey)}
|
||||
onSelect={(o) => props.onChangeThinking(o?.value)}
|
||||
placeholder={props.t("provider.custom.models.variants.thinking.placeholder")}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "4px",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{ "font-size": "var(--kilo-font-size-12)", "font-weight": "500", color: "var(--text-weak-base)" }}
|
||||
>
|
||||
{props.t("provider.custom.models.variants.splitReasoning.label")}
|
||||
</label>
|
||||
<Select
|
||||
options={SPLIT_REASONING_OPTIONS}
|
||||
current={SPLIT_REASONING_OPTIONS.find((o) => o.value === props.v.splitReasoning)}
|
||||
value={(o) => String(o.value)}
|
||||
label={(o) => props.t(o.labelKey)}
|
||||
onSelect={(o) => props.onChangeSplitReasoning(o?.value)}
|
||||
placeholder={props.t("provider.custom.models.variants.splitReasoning.placeholder")}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "4px",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{ "font-size": "var(--kilo-font-size-12)", "font-weight": "500", color: "var(--text-weak-base)" }}
|
||||
>
|
||||
{props.t("provider.custom.models.variants.reasoningEffort.label")}
|
||||
</label>
|
||||
<Select
|
||||
options={REASONING_EFFORT_OPTIONS}
|
||||
current={REASONING_EFFORT_OPTIONS.find((o) => o.value === props.v.reasoningEffort)}
|
||||
value={(o) => String(o.value)}
|
||||
label={(o) => props.t(o.labelKey)}
|
||||
onSelect={(o) => props.onChangeReasoningEffort(o?.value)}
|
||||
placeholder={props.t("provider.custom.models.variants.reasoningEffort.placeholder")}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "4px",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{ "font-size": "var(--kilo-font-size-12)", "font-weight": "500", color: "var(--text-weak-base)" }}
|
||||
>
|
||||
{props.t("provider.custom.models.variants.outputEffort.label")}
|
||||
</label>
|
||||
<Select
|
||||
options={OUTPUT_EFFORT_OPTIONS}
|
||||
current={OUTPUT_EFFORT_OPTIONS.find((o) => o.value === props.v.outputEffort)}
|
||||
value={(o) => String(o.value)}
|
||||
label={(o) => props.t(o.labelKey)}
|
||||
onSelect={(o) => props.onChangeOutputEffort(o?.value)}
|
||||
placeholder={props.t("provider.custom.models.variants.outputEffort.placeholder")}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "4px",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{ "font-size": "var(--kilo-font-size-12)", "font-weight": "500", color: "var(--text-weak-base)" }}
|
||||
>
|
||||
{props.t("provider.custom.models.variants.chatTemplateArgs.label")}
|
||||
</label>
|
||||
<Select
|
||||
options={CHAT_TEMPLATE_ARGS_OPTIONS}
|
||||
current={CHAT_TEMPLATE_ARGS_OPTIONS.find((o) => o.value === props.v.chatTemplateArgs)}
|
||||
value={(o) => String(o.value)}
|
||||
label={(o) => props.t(o.labelKey)}
|
||||
onSelect={(o) => props.onChangeChatTemplateArgs(o?.value)}
|
||||
placeholder={props.t("provider.custom.models.variants.chatTemplateArgs.placeholder")}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
type="button"
|
||||
icon="trash"
|
||||
variant="ghost"
|
||||
onClick={props.onRemove}
|
||||
aria-label={props.t("provider.custom.models.variants.remove")}
|
||||
style={{ "margin-bottom": "4px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ModelCardProps = {
|
||||
m: ModelEntry
|
||||
i: () => number
|
||||
errors: { id?: string; name?: string; variants?: Array<{ name?: string }> }
|
||||
t: Translator
|
||||
canRemove: boolean
|
||||
@@ -306,18 +49,11 @@ type ModelCardProps = {
|
||||
onChangeReasoning: (val: boolean) => void
|
||||
onChangeSupportsImages: (val: boolean) => void
|
||||
onRemove: () => void
|
||||
onAddVariant: () => void
|
||||
onRemoveVariant: (vi: number) => void
|
||||
onChangeVariantName: (vi: number, val: string) => void
|
||||
onChangeVariantEnableThinking: (vi: number, val: EnableThinkingValue) => void
|
||||
onChangeVariantThinking: (vi: number, val: ThinkingTypeValue) => void
|
||||
onChangeVariantSplitReasoning: (vi: number, val: SplitReasoningValue) => void
|
||||
onChangeVariantReasoningEffort: (vi: number, val: ReasoningEffortValue) => void
|
||||
onChangeVariantOutputEffort: (vi: number, val: OutputEffortValue) => void
|
||||
onChangeVariantChatTemplateArgs: (vi: number, val: ChatTemplateArgsValue) => void
|
||||
}
|
||||
|
||||
export function ModelCard(props: ModelCardProps) {
|
||||
const issue = () => props.errors.variants?.find((error) => error.name)?.name
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -399,39 +135,15 @@ export function ModelCard(props: ModelCardProps) {
|
||||
{props.t("provider.custom.models.modalities.image")}
|
||||
</label>
|
||||
|
||||
{/* Variants — only available when reasoning is enabled */}
|
||||
<Show when={props.m.reasoning}>
|
||||
<Show when={props.m.variants.length > 0}>
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "0" }}>
|
||||
<label
|
||||
style={{ "font-size": "var(--kilo-font-size-11)", "font-weight": "500", color: "var(--text-weak-base)" }}
|
||||
>
|
||||
{props.t("provider.custom.models.variants.label")}
|
||||
</label>
|
||||
<For each={props.m.variants}>
|
||||
{(v, vi) => (
|
||||
<VariantRow
|
||||
v={v}
|
||||
vi={vi}
|
||||
isFirst={() => vi() === 0}
|
||||
error={props.errors.variants?.[vi()]}
|
||||
t={props.t}
|
||||
onChangeName={(val) => props.onChangeVariantName(vi(), val)}
|
||||
onChangeEnableThinking={(val) => props.onChangeVariantEnableThinking(vi(), val)}
|
||||
onChangeThinking={(val) => props.onChangeVariantThinking(vi(), val)}
|
||||
onChangeSplitReasoning={(val) => props.onChangeVariantSplitReasoning(vi(), val)}
|
||||
onChangeReasoningEffort={(val) => props.onChangeVariantReasoningEffort(vi(), val)}
|
||||
onChangeOutputEffort={(val) => props.onChangeVariantOutputEffort(vi(), val)}
|
||||
onChangeChatTemplateArgs={(val) => props.onChangeVariantChatTemplateArgs(vi(), val)}
|
||||
onRemove={() => props.onRemoveVariant(vi())}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={props.onAddVariant}>
|
||||
{props.t("provider.custom.models.variants.add")}
|
||||
</Button>
|
||||
<Show when={issue()}>
|
||||
{(error) => (
|
||||
<span
|
||||
role="alert"
|
||||
style={{ "font-size": "var(--kilo-font-size-12)", color: "var(--vscode-errorForeground)" }}
|
||||
>
|
||||
{error()}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -57,8 +57,9 @@ const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
|
||||
|
||||
function checkVariant(v: VariantEntry, seen: Set<string>, t: Translator) {
|
||||
const n = v.name.trim()
|
||||
if (!n) return { name: t("provider.custom.error.required") }
|
||||
if (seen.has(n)) return { name: t("provider.custom.error.duplicate") }
|
||||
const path = `variants[${JSON.stringify(v.name)}]`
|
||||
if (!n) return { name: `${path}: ${t("provider.custom.error.required")}` }
|
||||
if (seen.has(n)) return { name: `${path}: ${t("provider.custom.error.duplicate")}` }
|
||||
seen.add(n)
|
||||
return { name: undefined }
|
||||
}
|
||||
@@ -105,6 +106,7 @@ function checkProviderID(id: string, editing: boolean, disabled: string[], exist
|
||||
}
|
||||
|
||||
function serializeVariant(v: VariantEntry): [string, Record<string, unknown>] {
|
||||
if (v.raw) return [v.name.trim(), v.raw]
|
||||
const cfg: Record<string, unknown> = {}
|
||||
if (v.enableThinking !== undefined) cfg.enable_thinking = v.enableThinking
|
||||
if (v.thinking !== undefined) cfg.thinking = { type: v.thinking }
|
||||
|
||||
@@ -28,6 +28,7 @@ import SandboxingTab from "./SandboxingTab"
|
||||
import * as Sandboxing from "./sandboxing"
|
||||
import { useServer } from "../../context/server"
|
||||
import type { MigrationSource } from "../../types/messages"
|
||||
import { configMessage } from "../../utils/open-config"
|
||||
|
||||
export interface SettingsProps {
|
||||
tab?: string
|
||||
@@ -66,34 +67,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||
}
|
||||
|
||||
const open = (scope: "local" | "global") => {
|
||||
const label =
|
||||
scope === "global" ? language.t("settings.config.scope.global") : language.t("settings.config.scope.local")
|
||||
vscode.postMessage({
|
||||
type: "openConfigFile",
|
||||
scope,
|
||||
labels: {
|
||||
scope: label,
|
||||
statusLoaded: language.t("settings.config.status.loaded"),
|
||||
statusLoadedLegacy: language.t("settings.config.status.loadedLegacy"),
|
||||
statusNotLoaded: language.t("settings.config.status.notLoaded"),
|
||||
statusCreate: language.t("settings.config.status.create"),
|
||||
title: language.t("settings.config.title", { scope: label }),
|
||||
placeholder: language.t("settings.config.placeholder"),
|
||||
noWorkspace: language.t("settings.config.noWorkspace"),
|
||||
openFailed: language.t("settings.config.openFailed", { scope: label, message: "{{message}}" }),
|
||||
sourceXdg: language.t("settings.config.source.xdg"),
|
||||
sourceHomeKilo: language.t("settings.config.source.homeKilo"),
|
||||
sourceHomeKilocode: language.t("settings.config.source.homeKilocode"),
|
||||
sourceHomeOpencode: language.t("settings.config.source.homeOpencode"),
|
||||
sourceEnvFile: language.t("settings.config.source.envFile"),
|
||||
sourceEnvDir: language.t("settings.config.source.envDir"),
|
||||
sourceEnvContent: language.t("settings.config.source.envContent"),
|
||||
sourceProjectKilo: language.t("settings.config.source.projectKilo"),
|
||||
sourceProjectRoot: language.t("settings.config.source.projectRoot"),
|
||||
sourceProjectKilocode: language.t("settings.config.source.projectKilocode"),
|
||||
sourceProjectOpencode: language.t("settings.config.source.projectOpencode"),
|
||||
},
|
||||
})
|
||||
vscode.postMessage(configMessage(scope, language.t))
|
||||
}
|
||||
|
||||
// Sync when the parent changes the tab prop (e.g. via navigate message)
|
||||
|
||||
@@ -50,12 +50,13 @@ export const ModelPreview: Component<Props> = (props) => {
|
||||
<Show when={m()}>
|
||||
{(model) => {
|
||||
const cost = () => model().cost
|
||||
const hasPricing = () => cost() && (cost()!.input > 0 || cost()!.output > 0)
|
||||
const bench = () => model().terminalBench
|
||||
const cachedText = () => {
|
||||
if (!cost()) return ""
|
||||
if (!cost() || !hasPricing()) return ""
|
||||
return fmtCachedPrice(cost()!) ?? language.t("model.preview.value.notSupported")
|
||||
}
|
||||
const avg = () => (cost() ? avgPrice(cost()!) : undefined)
|
||||
const avg = () => (cost() && hasPricing() ? avgPrice(cost()!) : undefined)
|
||||
const freeLabel = () => language.t("model.tag.free")
|
||||
const dataLabel = () => freeDataLabel(language.t("model.tag.free"), language.t("model.tag.dataCollected"))
|
||||
const autoLabel = () => autoSummary(model())
|
||||
@@ -139,8 +140,8 @@ export const ModelPreview: Component<Props> = (props) => {
|
||||
<span class="model-preview-value">{fmtDate(model().releaseDate!)}</span>
|
||||
</Show>
|
||||
|
||||
{/* Pricing — hidden for free models */}
|
||||
<Show when={cost() && !model().isFree}>
|
||||
{/* Pricing — hidden for free models or models without fixed pricing */}
|
||||
<Show when={cost() && !model().isFree && hasPricing()}>
|
||||
<span class="model-preview-label">{language.t("model.preview.label.input")}</span>
|
||||
<span class="model-preview-value">{fmtPrice(cost()!.input)}</span>
|
||||
<span class="model-preview-label">{language.t("model.preview.label.output")}</span>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Expects `n` in $/M tokens (as stored in model.cost.input / model.cost.output).
|
||||
*/
|
||||
export function fmtPrice(n: number): string {
|
||||
if (n < 0) return "—"
|
||||
if (n === 0) return "Free"
|
||||
if (n < 0.01) return `$${n.toFixed(4)}/1M`
|
||||
return `$${n.toFixed(2)}/1M`
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
export { KILO_GATEWAY_ID, PROVIDER_ORDER }
|
||||
|
||||
export const KILO_AUTO_SMALL_IDS = new Set(["kilo-auto/small", "auto-small"])
|
||||
export const KILO_AUTO_EFFICIENT_ID = "kilo-auto/efficient"
|
||||
const AUTO_FALLBACK = "Routes requests automatically."
|
||||
|
||||
interface Choice {
|
||||
@@ -24,15 +23,11 @@ export function isAuto(model: Pick<EnrichedModel, "providerID" | "id">): boolean
|
||||
)
|
||||
}
|
||||
|
||||
export function isAutoEfficient(model: Pick<EnrichedModel, "providerID" | "id">): boolean {
|
||||
return model.providerID === KILO_GATEWAY_ID && model.id === KILO_AUTO_EFFICIENT_ID
|
||||
}
|
||||
|
||||
export function autoChoices(
|
||||
model: Pick<EnrichedModel, "providerID" | "id" | "autoRouting">,
|
||||
catalog: readonly Pick<EnrichedModel, "id" | "name">[] = [],
|
||||
): readonly Choice[] {
|
||||
if (!isAutoEfficient(model)) return []
|
||||
if (!isAuto(model)) return []
|
||||
const ids = model.autoRouting?.models
|
||||
if (!ids?.length) return []
|
||||
const names = new Map(catalog.map((item) => [item.id, stripSubProviderPrefix(sanitizeName(item.name))]))
|
||||
|
||||
+2
-38
@@ -297,6 +297,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "بواسطة وضع الموافقة التلقائية (YOLO)",
|
||||
"ui.approval.source.session": "بواسطة قاعدة موافقة تلقائية للجلسة",
|
||||
"ui.approval.source.default": "افتراضيًا",
|
||||
"ui.approval.outsideWorkspace": "(خارج مساحة العمل: {{file}})",
|
||||
|
||||
"session.tab.review": "مراجعة",
|
||||
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
|
||||
@@ -442,44 +443,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "الاسم المعروض",
|
||||
"provider.custom.models.reasoning.label": "الاستدلال",
|
||||
"provider.custom.models.modalities.image": "صورة",
|
||||
"provider.custom.models.variants.label": "المتغيرات",
|
||||
"provider.custom.models.variants.add": "إضافة متغير",
|
||||
"provider.custom.models.variants.remove": "إزالة المتغير",
|
||||
"provider.custom.models.variants.name.label": "الاسم",
|
||||
"provider.custom.models.variants.name.placeholder": "على سبيل المثال: thinking",
|
||||
"provider.custom.models.variants.option.unset": "(غير محدد)",
|
||||
"provider.custom.models.variants.enableThinking.label": "تمكين التفكير (مثل Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "نوع التفكير (مثل Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label": "تفعيل التفكير عبر وسائط قالب الدردشة (مثل Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "جهد الاستدلال",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "إزالة النموذج",
|
||||
"provider.custom.models.add": "إضافة نموذج",
|
||||
"provider.custom.models.fetch.authError": "فشلت المصادقة. تحقق من مفتاح API أعلاه وحاول مرة أخرى.",
|
||||
@@ -493,6 +456,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "البحث في النماذج\u2026",
|
||||
"provider.custom.models.fetch.add": "إضافة {{count}} نموذج(نماذج)",
|
||||
"provider.custom.edit.title": "تعديل المزود",
|
||||
"provider.custom.edit.advanced": "تحرير الإعدادات المتقدمة في ملف إعداد JSON",
|
||||
"provider.custom.headers.label": "الرؤوس (اختياري)",
|
||||
"provider.custom.headers.key.label": "الرأس",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -307,6 +307,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "pelo modo de aprovação automática (YOLO)",
|
||||
"ui.approval.source.session": "por uma regra de aprovação automática da sessão",
|
||||
"ui.approval.source.default": "por padrão",
|
||||
"ui.approval.outsideWorkspace": "(fora do seu espaço de trabalho: {{file}})",
|
||||
|
||||
"session.tab.review": "Revisão",
|
||||
"session.review.filesChanged": "{{count}} Arquivos Alterados",
|
||||
@@ -453,45 +454,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Nome de Exibição",
|
||||
"provider.custom.models.reasoning.label": "Raciocínio",
|
||||
"provider.custom.models.modalities.image": "Imagem",
|
||||
"provider.custom.models.variants.label": "Variantes",
|
||||
"provider.custom.models.variants.add": "Adicionar variante",
|
||||
"provider.custom.models.variants.remove": "Remover variante",
|
||||
"provider.custom.models.variants.name.label": "Nome",
|
||||
"provider.custom.models.variants.name.placeholder": "ex. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(não definido)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Ativar pensamento (ex. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Tipo de pensamento (ex. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Ativar pensamento via args do template de chat (ex. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Esforço de raciocínio",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Remover modelo",
|
||||
"provider.custom.models.add": "Adicionar modelo",
|
||||
"provider.custom.models.fetch.authError": "Falha na autenticação. Verifique a chave de API acima e tente novamente.",
|
||||
@@ -505,6 +467,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Pesquisar modelos\u2026",
|
||||
"provider.custom.models.fetch.add": "Adicionar {{count}} modelo(s)",
|
||||
"provider.custom.edit.title": "Editar provedor",
|
||||
"provider.custom.edit.advanced": "Editar configurações avançadas no arquivo de configuração JSON",
|
||||
"provider.custom.headers.label": "Headers (opcional)",
|
||||
"provider.custom.headers.key.label": "Cabeçalho",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -305,6 +305,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "režimom automatskog odobravanja (YOLO)",
|
||||
"ui.approval.source.session": "pravilom automatskog odobravanja sesije",
|
||||
"ui.approval.source.default": "podrazumevano",
|
||||
"ui.approval.outsideWorkspace": "(izvan vašeg radnog prostora: {{file}})",
|
||||
|
||||
"session.tab.review": "Pregled",
|
||||
"session.review.filesChanged": "Izmijenjeno {{count}} datoteka",
|
||||
@@ -495,45 +496,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Naziv za prikaz",
|
||||
"provider.custom.models.reasoning.label": "Zaključivanje",
|
||||
"provider.custom.models.modalities.image": "Slika",
|
||||
"provider.custom.models.variants.label": "Varijante",
|
||||
"provider.custom.models.variants.add": "Dodaj varijantu",
|
||||
"provider.custom.models.variants.remove": "Ukloni varijantu",
|
||||
"provider.custom.models.variants.name.label": "Ime",
|
||||
"provider.custom.models.variants.name.placeholder": "npr. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(nije postavljeno)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Omogući razmišljanje (npr. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Vrsta razmišljanja (npr. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Omogući razmišljanje preko argumenata chat predloška (npr. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Napor zaključivanja",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Ukloni model",
|
||||
"provider.custom.models.add": "Dodaj model",
|
||||
"provider.custom.models.fetch.authError":
|
||||
@@ -548,6 +510,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Pretraži modele\u2026",
|
||||
"provider.custom.models.fetch.add": "Dodaj {{count}} model(a)",
|
||||
"provider.custom.edit.title": "Uredi provajdera",
|
||||
"provider.custom.edit.advanced": "Uredite napredne postavke u JSON konfiguracijskoj datoteci",
|
||||
"provider.custom.headers.label": "Zaglavlja (opcionalno)",
|
||||
"provider.custom.headers.key.label": "Zaglavlje",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -304,6 +304,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "af automatisk godkendelse (YOLO)",
|
||||
"ui.approval.source.session": "af en session-autogodkendelsesregel",
|
||||
"ui.approval.source.default": "som standard",
|
||||
"ui.approval.outsideWorkspace": "(uden for dit arbejdsområde: {{file}})",
|
||||
|
||||
"session.tab.review": "Gennemgang",
|
||||
"session.review.filesChanged": "{{count}} Filer ændret",
|
||||
@@ -493,45 +494,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Visningsnavn",
|
||||
"provider.custom.models.reasoning.label": "Ræsonnement",
|
||||
"provider.custom.models.modalities.image": "Billede",
|
||||
"provider.custom.models.variants.label": "Varianter",
|
||||
"provider.custom.models.variants.add": "Tilføj variant",
|
||||
"provider.custom.models.variants.remove": "Fjern variant",
|
||||
"provider.custom.models.variants.name.label": "Navn",
|
||||
"provider.custom.models.variants.name.placeholder": "f.eks. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(ikke angivet)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Aktivér tænkning (f.eks. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Tænkningstype (f.eks. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Aktivér tænkning via chat-skabelonargs (f.eks. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Ræsonnementsindsats",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Fjern model",
|
||||
"provider.custom.models.add": "Tilføj model",
|
||||
"provider.custom.models.fetch.authError": "Godkendelse mislykkedes. Kontrollér API-nøglen ovenfor, og prøv igen.",
|
||||
@@ -545,6 +507,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Søg modeller\u2026",
|
||||
"provider.custom.models.fetch.add": "Tilføj {{count}} model(ler)",
|
||||
"provider.custom.edit.title": "Rediger udbyder",
|
||||
"provider.custom.edit.advanced": "Rediger avancerede indstillinger i JSON-konfigurationsfilen",
|
||||
"provider.custom.headers.label": "Headers (valgfrit)",
|
||||
"provider.custom.headers.key.label": "Header",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
@@ -313,6 +313,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "durch den Auto-Genehmigungsmodus (YOLO)",
|
||||
"ui.approval.source.session": "durch eine Sitzungs-Auto-Genehmigungsregel",
|
||||
"ui.approval.source.default": "standardmäßig",
|
||||
"ui.approval.outsideWorkspace": "(außerhalb deines Arbeitsbereichs: {{file}})",
|
||||
|
||||
"session.tab.review": "Überprüfung",
|
||||
"session.review.filesChanged": "{{count}} Dateien geändert",
|
||||
@@ -503,45 +504,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Anzeigename",
|
||||
"provider.custom.models.reasoning.label": "Schlussfolgerung",
|
||||
"provider.custom.models.modalities.image": "Bild",
|
||||
"provider.custom.models.variants.label": "Varianten",
|
||||
"provider.custom.models.variants.add": "Variante hinzufügen",
|
||||
"provider.custom.models.variants.remove": "Variante entfernen",
|
||||
"provider.custom.models.variants.name.label": "Name",
|
||||
"provider.custom.models.variants.name.placeholder": "z.B. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(nicht festgelegt)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Nachdenken aktivieren (z.B. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Art des Nachdenkens (z.B. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Nachdenken über Chat-Vorlagenargumente aktivieren (z.B. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Reasoning-Aufwand",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Modell entfernen",
|
||||
"provider.custom.models.add": "Modell hinzufügen",
|
||||
"provider.custom.models.fetch.authError":
|
||||
@@ -556,6 +518,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Modelle suchen\u2026",
|
||||
"provider.custom.models.fetch.add": "{{count}} Modell(e) hinzufügen",
|
||||
"provider.custom.edit.title": "Anbieter bearbeiten",
|
||||
"provider.custom.edit.advanced": "Erweiterte Einstellungen in der JSON-Konfigurationsdatei bearbeiten",
|
||||
"provider.custom.headers.label": "Header (optional)",
|
||||
"provider.custom.headers.key.label": "Kopfzeile",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
@@ -302,6 +302,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "by auto-approve (YOLO) mode",
|
||||
"ui.approval.source.session": "by a session auto-approve rule",
|
||||
"ui.approval.source.default": "by default",
|
||||
"ui.approval.outsideWorkspace": "(outside your workspace: {{file}})",
|
||||
|
||||
"session.tab.review": "Review",
|
||||
"session.review.filesChanged": "{{count}} Files Changed",
|
||||
@@ -407,45 +408,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Display Name",
|
||||
"provider.custom.models.reasoning.label": "Reasoning",
|
||||
"provider.custom.models.modalities.image": "Image",
|
||||
"provider.custom.models.variants.label": "Variants",
|
||||
"provider.custom.models.variants.add": "Add variant",
|
||||
"provider.custom.models.variants.remove": "Remove variant",
|
||||
"provider.custom.models.variants.name.label": "Name",
|
||||
"provider.custom.models.variants.name.placeholder": "e.g. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(not set)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Enable thinking (e.g. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Thinking type (e.g. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Enable thinking via chat template args (e.g. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Reasoning effort",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Remove model",
|
||||
"provider.custom.models.add": "Add model",
|
||||
"provider.custom.models.fetch.authError": "Authentication failed. Check the API key above and try again.",
|
||||
@@ -459,6 +421,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Search models\u2026",
|
||||
"provider.custom.models.fetch.add": "Add {{count}} model(s)",
|
||||
"provider.custom.edit.title": "Edit provider",
|
||||
"provider.custom.edit.advanced": "Edit advanced settings in the JSON config file",
|
||||
"provider.custom.headers.label": "Headers (optional)",
|
||||
"provider.custom.headers.key.label": "Header",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -308,6 +308,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "por el modo de aprobación automática (YOLO)",
|
||||
"ui.approval.source.session": "por una regla de aprobación automática de sesión",
|
||||
"ui.approval.source.default": "de forma predeterminada",
|
||||
"ui.approval.outsideWorkspace": "(fuera de tu espacio de trabajo: {{file}})",
|
||||
|
||||
"session.tab.review": "Revisión",
|
||||
"session.review.filesChanged": "{{count}} Archivos Cambiados",
|
||||
@@ -496,45 +497,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Nombre para mostrar",
|
||||
"provider.custom.models.reasoning.label": "Razonamiento",
|
||||
"provider.custom.models.modalities.image": "Imagen",
|
||||
"provider.custom.models.variants.label": "Variantes",
|
||||
"provider.custom.models.variants.add": "Añadir variante",
|
||||
"provider.custom.models.variants.remove": "Eliminar variante",
|
||||
"provider.custom.models.variants.name.label": "Nombre",
|
||||
"provider.custom.models.variants.name.placeholder": "p. ej. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(no establecido)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Habilitar pensamiento (p. ej. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Tipo de pensamiento (p. ej. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Activar pensamiento mediante args de plantilla de chat (ej. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Esfuerzo de razonamiento",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Eliminar modelo",
|
||||
"provider.custom.models.add": "Añadir modelo",
|
||||
"provider.custom.models.fetch.authError":
|
||||
@@ -549,6 +511,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Buscar modelos\u2026",
|
||||
"provider.custom.models.fetch.add": "Añadir {{count}} modelo(s)",
|
||||
"provider.custom.edit.title": "Editar proveedor",
|
||||
"provider.custom.edit.advanced": "Editar la configuración avanzada en el archivo de configuración JSON",
|
||||
"provider.custom.headers.label": "Headers (opcional)",
|
||||
"provider.custom.headers.key.label": "Header",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -302,6 +302,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "توسط حالت تأیید خودکار (YOLO)",
|
||||
"ui.approval.source.session": "توسط قانون تأیید خودکار جلسه",
|
||||
"ui.approval.source.default": "بهطور پیشفرض",
|
||||
"ui.approval.outsideWorkspace": "(خارج از فضای کاری شما: {{file}})",
|
||||
|
||||
"session.tab.review": "بررسی",
|
||||
"session.review.filesChanged": "{{count}} فایل تغییر یافته",
|
||||
@@ -408,45 +409,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "نام نمایشی",
|
||||
"provider.custom.models.reasoning.label": "استدلال",
|
||||
"provider.custom.models.modalities.image": "تصویر",
|
||||
"provider.custom.models.variants.label": "نسخههای متغیر",
|
||||
"provider.custom.models.variants.add": "افزودن نسخه متغیر",
|
||||
"provider.custom.models.variants.remove": "حذف نسخه متغیر",
|
||||
"provider.custom.models.variants.name.label": "نام",
|
||||
"provider.custom.models.variants.name.placeholder": "مثلاً thinking",
|
||||
"provider.custom.models.variants.option.unset": "(تنظیم نشده)",
|
||||
"provider.custom.models.variants.enableThinking.label": "فعالسازی تفکر (مثلاً Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "نوع تفکر (مثلاً Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "فعال",
|
||||
"provider.custom.models.variants.thinking.disabled": "غیرفعال",
|
||||
"provider.custom.models.variants.thinking.adaptive": "تطبیقی",
|
||||
"provider.custom.models.variants.splitReasoning.label": "تقسیم استدلال (لازم برای مثلاً MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"فعالسازی تفکر از طریق آرگومانهای قالب چت (مثلاً Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "سطح استدلال",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "هیچ",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "حداقل",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "کم",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "متوسط",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "زیاد",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "تلاش خروجی (مثلاً Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "تلاش",
|
||||
"provider.custom.models.variants.outputEffort.low": "کم",
|
||||
"provider.custom.models.variants.outputEffort.medium": "متوسط",
|
||||
"provider.custom.models.variants.outputEffort.high": "زیاد",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "حداکثر",
|
||||
"provider.custom.models.remove": "حذف مدل",
|
||||
"provider.custom.models.add": "افزودن مدل",
|
||||
"provider.custom.models.fetch.authError": "احراز هویت ناموفق بود. کلید API بالا را بررسی کرده و دوباره امتحان کنید.",
|
||||
@@ -460,6 +422,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "جستجوی مدلها…",
|
||||
"provider.custom.models.fetch.add": "افزودن {{count}} مدل",
|
||||
"provider.custom.edit.title": "ویرایش ارائهدهنده",
|
||||
"provider.custom.edit.advanced": "ویرایش تنظیمات پیشرفته در فایل پیکربندی JSON",
|
||||
"provider.custom.headers.label": "هدرها (اختیاری)",
|
||||
"provider.custom.headers.key.label": "هدر",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -307,6 +307,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "par le mode d'approbation automatique (YOLO)",
|
||||
"ui.approval.source.session": "par une règle d'approbation automatique de session",
|
||||
"ui.approval.source.default": "par défaut",
|
||||
"ui.approval.outsideWorkspace": "(hors de votre espace de travail : {{file}})",
|
||||
|
||||
"session.tab.review": "Revue",
|
||||
"session.review.filesChanged": "{{count}} fichiers modifiés",
|
||||
@@ -497,45 +498,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Nom d'affichage",
|
||||
"provider.custom.models.reasoning.label": "Raisonnement",
|
||||
"provider.custom.models.modalities.image": "Image",
|
||||
"provider.custom.models.variants.label": "Variantes",
|
||||
"provider.custom.models.variants.add": "Ajouter une variante",
|
||||
"provider.custom.models.variants.remove": "Supprimer la variante",
|
||||
"provider.custom.models.variants.name.label": "Nom",
|
||||
"provider.custom.models.variants.name.placeholder": "ex. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(non défini)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Activer la réflexion (ex. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Type de réflexion (ex. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Activer la réflexion via les args du modèle de chat (ex. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Effort de raisonnement",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Supprimer le modèle",
|
||||
"provider.custom.models.add": "Ajouter un modèle",
|
||||
"provider.custom.models.fetch.authError": "Échec de l'authentification. Vérifiez la clé API ci-dessus et réessayez.",
|
||||
@@ -549,6 +511,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Rechercher des modèles\u2026",
|
||||
"provider.custom.models.fetch.add": "Ajouter {{count}} modèle(s)",
|
||||
"provider.custom.edit.title": "Modifier le fournisseur",
|
||||
"provider.custom.edit.advanced": "Modifier les paramètres avancés dans le fichier de configuration JSON",
|
||||
"provider.custom.headers.label": "En-têtes (optionnel)",
|
||||
"provider.custom.headers.key.label": "En-tête",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -219,6 +219,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "dalla modalità di approvazione automatica (YOLO)",
|
||||
"ui.approval.source.session": "da una regola di approvazione automatica della sessione",
|
||||
"ui.approval.source.default": "per impostazione predefinita",
|
||||
"ui.approval.outsideWorkspace": "(fuori dall'area di lavoro: {{file}})",
|
||||
"session.tab.review": "Revisione",
|
||||
"session.review.filesChanged": "{{count}} file modificati",
|
||||
"session.review.loadingChanges": "Caricamento modifiche...",
|
||||
@@ -317,45 +318,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Nome visualizzato",
|
||||
"provider.custom.models.reasoning.label": "Reasoning",
|
||||
"provider.custom.models.modalities.image": "Immagine",
|
||||
"provider.custom.models.variants.label": "Variants",
|
||||
"provider.custom.models.variants.add": "Aggiungi variante",
|
||||
"provider.custom.models.variants.remove": "Rimuovi variante",
|
||||
"provider.custom.models.variants.name.label": "Nome",
|
||||
"provider.custom.models.variants.name.placeholder": "es. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(non impostato)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Abilita thinking (es. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Tipo thinking (es. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Enable thinking via chat template args (e.g. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Sforzo di ragionamento",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Rimuovi modello",
|
||||
"provider.custom.models.add": "Aggiungi modello",
|
||||
"provider.custom.models.fetch.authError": "Autenticazione non riuscita. Controlla l'API key sopra e riprova.",
|
||||
@@ -369,6 +331,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Cerca modelli...",
|
||||
"provider.custom.models.fetch.add": "Aggiungi {{count}} modelli",
|
||||
"provider.custom.edit.title": "Modifica provider",
|
||||
"provider.custom.edit.advanced": "Modifica le impostazioni avanzate nel file di configurazione JSON",
|
||||
"provider.custom.headers.label": "Header (opzionali)",
|
||||
"provider.custom.headers.key.label": "Header",
|
||||
"provider.custom.headers.key.placeholder": "Nome-Header",
|
||||
|
||||
+2
-38
@@ -304,6 +304,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "自動承認(YOLO)モードによって",
|
||||
"ui.approval.source.session": "セッションの自動承認ルールによって",
|
||||
"ui.approval.source.default": "デフォルトで",
|
||||
"ui.approval.outsideWorkspace": "(ワークスペース外:{{file}})",
|
||||
|
||||
"session.tab.review": "レビュー",
|
||||
"session.review.filesChanged": "{{count}} ファイル変更",
|
||||
@@ -489,44 +490,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "表示名",
|
||||
"provider.custom.models.reasoning.label": "推論",
|
||||
"provider.custom.models.modalities.image": "画像",
|
||||
"provider.custom.models.variants.label": "バリアント",
|
||||
"provider.custom.models.variants.add": "バリアントを追加",
|
||||
"provider.custom.models.variants.remove": "バリアントを削除",
|
||||
"provider.custom.models.variants.name.label": "名前",
|
||||
"provider.custom.models.variants.name.placeholder": "例: thinking",
|
||||
"provider.custom.models.variants.option.unset": "(未設定)",
|
||||
"provider.custom.models.variants.enableThinking.label": "思考を有効にする (例: Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "思考タイプ (例: Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label": "チャットテンプレート引数で思考を有効化 (例: Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "推論エフォート",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "モデルを削除",
|
||||
"provider.custom.models.add": "モデルを追加",
|
||||
"provider.custom.models.fetch.authError": "認証に失敗しました。上記のAPIキーを確認して再試行してください。",
|
||||
@@ -540,6 +503,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "モデルを検索\u2026",
|
||||
"provider.custom.models.fetch.add": "{{count}}個のモデルを追加",
|
||||
"provider.custom.edit.title": "プロバイダーを編集",
|
||||
"provider.custom.edit.advanced": "JSON 設定ファイルで詳細設定を編集",
|
||||
"provider.custom.headers.label": "ヘッダー(任意)",
|
||||
"provider.custom.headers.key.label": "ヘッダー",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-38
@@ -305,6 +305,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "자동 승인(YOLO) 모드에 의해",
|
||||
"ui.approval.source.session": "세션 자동 승인 규칙에 의해",
|
||||
"ui.approval.source.default": "기본값으로",
|
||||
"ui.approval.outsideWorkspace": "(작업 영역 외부: {{file}})",
|
||||
|
||||
"session.tab.review": "검토",
|
||||
"session.review.filesChanged": "{{count}}개 파일 변경됨",
|
||||
@@ -450,44 +451,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "표시 이름",
|
||||
"provider.custom.models.reasoning.label": "추론",
|
||||
"provider.custom.models.modalities.image": "이미지",
|
||||
"provider.custom.models.variants.label": "변형",
|
||||
"provider.custom.models.variants.add": "변형 추가",
|
||||
"provider.custom.models.variants.remove": "변형 제거",
|
||||
"provider.custom.models.variants.name.label": "이름",
|
||||
"provider.custom.models.variants.name.placeholder": "예: thinking",
|
||||
"provider.custom.models.variants.option.unset": "(설정되지 않음)",
|
||||
"provider.custom.models.variants.enableThinking.label": "사고 활성화 (예: Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "사고 유형 (예: Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label": "채팅 템플릿 인수로 사고 활성화 (예: Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "추론 노력",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "모델 제거",
|
||||
"provider.custom.models.add": "모델 추가",
|
||||
"provider.custom.models.fetch.authError": "인증에 실패했습니다. 위의 API 키를 확인하고 다시 시도하세요.",
|
||||
@@ -501,6 +464,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "모델 검색\u2026",
|
||||
"provider.custom.models.fetch.add": "{{count}}개 모델 추가",
|
||||
"provider.custom.edit.title": "공급자 편집",
|
||||
"provider.custom.edit.advanced": "JSON 구성 파일에서 고급 설정 편집",
|
||||
"provider.custom.headers.label": "헤더 (선택사항)",
|
||||
"provider.custom.headers.key.label": "헤더",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -308,6 +308,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "door de automatische goedkeuringsmodus (YOLO)",
|
||||
"ui.approval.source.session": "door een sessie-automatische-goedkeuringsregel",
|
||||
"ui.approval.source.default": "standaard",
|
||||
"ui.approval.outsideWorkspace": "(buiten je werkruimte: {{file}})",
|
||||
|
||||
"session.tab.review": "Beoordelen",
|
||||
"session.review.filesChanged": "{{count}} bestanden gewijzigd",
|
||||
@@ -446,45 +447,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Weergavenaam",
|
||||
"provider.custom.models.reasoning.label": "Redeneren",
|
||||
"provider.custom.models.modalities.image": "Afbeelding",
|
||||
"provider.custom.models.variants.label": "Varianten",
|
||||
"provider.custom.models.variants.add": "Variant toevoegen",
|
||||
"provider.custom.models.variants.remove": "Variant verwijderen",
|
||||
"provider.custom.models.variants.name.label": "Naam",
|
||||
"provider.custom.models.variants.name.placeholder": "bijv. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(niet ingesteld)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Denken inschakelen (bijv. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Denktype (bijv. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Nadenken inschakelen via chat template args (bijv. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Redeneerinspanning",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Model verwijderen",
|
||||
"provider.custom.models.add": "Model toevoegen",
|
||||
"provider.custom.models.fetch.authError":
|
||||
@@ -499,6 +461,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Modellen zoeken\u2026",
|
||||
"provider.custom.models.fetch.add": "{{count}} model(len) toevoegen",
|
||||
"provider.custom.edit.title": "Provider bewerken",
|
||||
"provider.custom.edit.advanced": "Geavanceerde instellingen bewerken in het JSON-configuratiebestand",
|
||||
"provider.custom.headers.label": "Headers (optioneel)",
|
||||
"provider.custom.headers.key.label": "Header",
|
||||
"provider.custom.headers.key.placeholder": "Header-Naam",
|
||||
|
||||
+2
-39
@@ -311,6 +311,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "av automatisk godkjenning (YOLO)",
|
||||
"ui.approval.source.session": "av en økt-autogodkjenningsregel",
|
||||
"ui.approval.source.default": "som standard",
|
||||
"ui.approval.outsideWorkspace": "(utenfor arbeidsområdet ditt: {{file}})",
|
||||
|
||||
"session.tab.review": "Gjennomgang",
|
||||
"session.review.filesChanged": "{{count}} filer endret",
|
||||
@@ -456,45 +457,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Visningsnavn",
|
||||
"provider.custom.models.reasoning.label": "Resonnering",
|
||||
"provider.custom.models.modalities.image": "Bilde",
|
||||
"provider.custom.models.variants.label": "Varianter",
|
||||
"provider.custom.models.variants.add": "Legg til variant",
|
||||
"provider.custom.models.variants.remove": "Fjern variant",
|
||||
"provider.custom.models.variants.name.label": "Navn",
|
||||
"provider.custom.models.variants.name.placeholder": "f.eks. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(ikke angitt)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Aktiver tenkning (f.eks. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Tenkningstype (f.eks. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Aktiver tenkning via chat-malargumenter (f.eks. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Resonneringsinnsats",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Fjern modell",
|
||||
"provider.custom.models.add": "Legg til modell",
|
||||
"provider.custom.models.fetch.authError": "Autentisering mislyktes. Sjekk API-nøkkelen ovenfor og prøv igjen.",
|
||||
@@ -508,6 +470,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Søk etter modeller\u2026",
|
||||
"provider.custom.models.fetch.add": "Legg til {{count}} modell(er)",
|
||||
"provider.custom.edit.title": "Rediger leverandør",
|
||||
"provider.custom.edit.advanced": "Rediger avanserte innstillinger i JSON-konfigurasjonsfilen",
|
||||
"provider.custom.headers.label": "Headere (valgfritt)",
|
||||
"provider.custom.headers.key.label": "Header",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -305,6 +305,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "przez tryb automatycznego zatwierdzania (YOLO)",
|
||||
"ui.approval.source.session": "przez regułę automatycznego zatwierdzania sesji",
|
||||
"ui.approval.source.default": "domyślnie",
|
||||
"ui.approval.outsideWorkspace": "(poza obszarem roboczym: {{file}})",
|
||||
|
||||
"session.tab.review": "Przegląd",
|
||||
"session.review.filesChanged": "Zmieniono {{count}} plików",
|
||||
@@ -451,45 +452,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Nazwa wyświetlana",
|
||||
"provider.custom.models.reasoning.label": "Rozumowanie",
|
||||
"provider.custom.models.modalities.image": "Obraz",
|
||||
"provider.custom.models.variants.label": "Warianty",
|
||||
"provider.custom.models.variants.add": "Dodaj wariant",
|
||||
"provider.custom.models.variants.remove": "Usuń wariant",
|
||||
"provider.custom.models.variants.name.label": "Nazwa",
|
||||
"provider.custom.models.variants.name.placeholder": "np. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(nie ustawiono)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Włącz myślenie (np. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Typ myślenia (np. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Włącz myślenie przez argumenty szablonu czatu (np. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Wysiłek rozumowania",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Usuń model",
|
||||
"provider.custom.models.add": "Dodaj model",
|
||||
"provider.custom.models.fetch.authError":
|
||||
@@ -504,6 +466,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Szukaj modeli\u2026",
|
||||
"provider.custom.models.fetch.add": "Dodaj {{count}} model(i)",
|
||||
"provider.custom.edit.title": "Edytuj dostawcę",
|
||||
"provider.custom.edit.advanced": "Edytuj ustawienia zaawansowane w pliku konfiguracji JSON",
|
||||
"provider.custom.headers.label": "Nagłówki (opcjonalnie)",
|
||||
"provider.custom.headers.key.label": "Nagłówek",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -303,6 +303,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "режимом автоодобрения (YOLO)",
|
||||
"ui.approval.source.session": "правилом автоодобрения сессии",
|
||||
"ui.approval.source.default": "по умолчанию",
|
||||
"ui.approval.outsideWorkspace": "(за пределами вашей рабочей области: {{file}})",
|
||||
|
||||
"session.tab.review": "Обзор",
|
||||
"session.review.filesChanged": "{{count}} файлов изменено",
|
||||
@@ -490,45 +491,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Отображаемое имя",
|
||||
"provider.custom.models.reasoning.label": "Рассуждение",
|
||||
"provider.custom.models.modalities.image": "Изображение",
|
||||
"provider.custom.models.variants.label": "Варианты",
|
||||
"provider.custom.models.variants.add": "Добавить вариант",
|
||||
"provider.custom.models.variants.remove": "Удалить вариант",
|
||||
"provider.custom.models.variants.name.label": "Имя",
|
||||
"provider.custom.models.variants.name.placeholder": "напр. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(не задано)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Включить мышление (напр. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Тип мышления (напр. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Включить размышление через аргументы шаблона чата (напр. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Усилие рассуждения",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Удалить модель",
|
||||
"provider.custom.models.add": "Добавить модель",
|
||||
"provider.custom.models.fetch.authError": "Ошибка аутентификации. Проверьте API-ключ выше и попробуйте снова.",
|
||||
@@ -542,6 +504,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Поиск моделей\u2026",
|
||||
"provider.custom.models.fetch.add": "Добавить {{count}} модель(ей)",
|
||||
"provider.custom.edit.title": "Редактировать провайдера",
|
||||
"provider.custom.edit.advanced": "Изменить расширенные настройки в файле конфигурации JSON",
|
||||
"provider.custom.headers.label": "Заголовки (необязательно)",
|
||||
"provider.custom.headers.key.label": "Заголовок",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -302,6 +302,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "โดยโหมดอนุมัติอัตโนมัติ (YOLO)",
|
||||
"ui.approval.source.session": "โดยกฎอนุมัติอัตโนมัติของเซสชัน",
|
||||
"ui.approval.source.default": "ตามค่าเริ่มต้น",
|
||||
"ui.approval.outsideWorkspace": "(นอกพื้นที่ทำงานของคุณ: {{file}})",
|
||||
|
||||
"session.tab.review": "ตรวจสอบ",
|
||||
"session.review.filesChanged": "{{count}} ไฟล์ที่เปลี่ยนแปลง",
|
||||
@@ -487,45 +488,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "ชื่อที่แสดง",
|
||||
"provider.custom.models.reasoning.label": "การใช้เหตุผล",
|
||||
"provider.custom.models.modalities.image": "รูปภาพ",
|
||||
"provider.custom.models.variants.label": "รูปแบบ",
|
||||
"provider.custom.models.variants.add": "เพิ่มรูปแบบ",
|
||||
"provider.custom.models.variants.remove": "ลบรูปแบบ",
|
||||
"provider.custom.models.variants.name.label": "ชื่อ",
|
||||
"provider.custom.models.variants.name.placeholder": "เช่น thinking",
|
||||
"provider.custom.models.variants.option.unset": "(ไม่ได้ตั้งค่า)",
|
||||
"provider.custom.models.variants.enableThinking.label": "เปิดใช้งานการคิด (เช่น Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "ประเภทการคิด (เช่น Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"เปิดใช้งานการคิดผ่านอาร์กิวเมนต์เทมเพลตแชท (เช่น Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "ระดับการใช้เหตุผล",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "ลบโมเดล",
|
||||
"provider.custom.models.add": "เพิ่มโมเดล",
|
||||
"provider.custom.models.fetch.authError": "การยืนยันตัวตนล้มเหลว ตรวจสอบคีย์ API ด้านบนแล้วลองอีกครั้ง",
|
||||
@@ -539,6 +501,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "ค้นหาโมเดล\u2026",
|
||||
"provider.custom.models.fetch.add": "เพิ่ม {{count}} โมเดล",
|
||||
"provider.custom.edit.title": "แก้ไขผู้ให้บริการ",
|
||||
"provider.custom.edit.advanced": "แก้ไขการตั้งค่าขั้นสูงในไฟล์การกำหนดค่า JSON",
|
||||
"provider.custom.headers.label": "ส่วนหัว (ไม่จำเป็น)",
|
||||
"provider.custom.headers.key.label": "ส่วนหัว",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-39
@@ -303,6 +303,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "otomatik onay (YOLO) modu tarafından",
|
||||
"ui.approval.source.session": "bir oturum otomatik onay kuralı tarafından",
|
||||
"ui.approval.source.default": "varsayılan olarak",
|
||||
"ui.approval.outsideWorkspace": "(çalışma alanınızın dışında: {{file}})",
|
||||
|
||||
"session.tab.review": "İnceleme",
|
||||
"session.review.filesChanged": "{{count}} Dosya Değişti",
|
||||
@@ -441,45 +442,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Görünen Ad",
|
||||
"provider.custom.models.reasoning.label": "Akıl Yürütme",
|
||||
"provider.custom.models.modalities.image": "Görüntü",
|
||||
"provider.custom.models.variants.label": "Varyantlar",
|
||||
"provider.custom.models.variants.add": "Varyant ekle",
|
||||
"provider.custom.models.variants.remove": "Varyantı kaldır",
|
||||
"provider.custom.models.variants.name.label": "Ad",
|
||||
"provider.custom.models.variants.name.placeholder": "örn. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(ayarlanmadı)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Düşünmeyi etkinleştir (örn. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Düşünme türü (örn. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Sohbet şablonu argümanları ile düşünmeyi etkinleştir (ör. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Akıl yürütme çabası",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Modeli kaldır",
|
||||
"provider.custom.models.add": "Model ekle",
|
||||
"provider.custom.models.fetch.authError":
|
||||
@@ -494,6 +456,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Model ara\u2026",
|
||||
"provider.custom.models.fetch.add": "{{count}} model ekle",
|
||||
"provider.custom.edit.title": "Sağlayıcıyı düzenle",
|
||||
"provider.custom.edit.advanced": "JSON yapılandırma dosyasında gelişmiş ayarları düzenle",
|
||||
"provider.custom.headers.label": "Başlıklar (isteğe bağlı)",
|
||||
"provider.custom.headers.key.label": "Başlık",
|
||||
"provider.custom.headers.key.placeholder": "Başlık-Adı",
|
||||
|
||||
+2
-39
@@ -307,6 +307,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "режимом автосхвалення (YOLO)",
|
||||
"ui.approval.source.session": "правилом автосхвалення сесії",
|
||||
"ui.approval.source.default": "за замовчуванням",
|
||||
"ui.approval.outsideWorkspace": "(за межами вашого робочого простору: {{file}})",
|
||||
|
||||
"session.tab.review": "Огляд",
|
||||
"session.review.filesChanged": "{{count}} файлів змінено",
|
||||
@@ -445,45 +446,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "Відображувана назва",
|
||||
"provider.custom.models.reasoning.label": "Міркування",
|
||||
"provider.custom.models.modalities.image": "Зображення",
|
||||
"provider.custom.models.variants.label": "Варіанти",
|
||||
"provider.custom.models.variants.add": "Додати варіант",
|
||||
"provider.custom.models.variants.remove": "Видалити варіант",
|
||||
"provider.custom.models.variants.name.label": "Ім'я",
|
||||
"provider.custom.models.variants.name.placeholder": "напр. thinking",
|
||||
"provider.custom.models.variants.option.unset": "(не встановлено)",
|
||||
"provider.custom.models.variants.enableThinking.label": "Увімкнути мислення (напр. Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "Тип мислення (напр. Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label":
|
||||
"Увімкнути мислення через аргументи шаблону чату (напр. Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "Зусилля міркування",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "Видалити модель",
|
||||
"provider.custom.models.add": "Додати модель",
|
||||
"provider.custom.models.fetch.authError": "Автентифікація не вдалася. Перевірте API-ключ вище і спробуйте ще раз.",
|
||||
@@ -497,6 +459,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "Пошук моделей\u2026",
|
||||
"provider.custom.models.fetch.add": "Додати {{count}} моделей",
|
||||
"provider.custom.edit.title": "Редагувати провайдера",
|
||||
"provider.custom.edit.advanced": "Редагувати розширені налаштування у файлі конфігурації JSON",
|
||||
"provider.custom.headers.label": "Заголовки (необов'язково)",
|
||||
"provider.custom.headers.key.label": "Заголовок",
|
||||
"provider.custom.headers.key.placeholder": "Назва-заголовка",
|
||||
|
||||
+2
-38
@@ -292,6 +292,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "由自动批准(YOLO)模式",
|
||||
"ui.approval.source.session": "由会话自动批准规则",
|
||||
"ui.approval.source.default": "默认",
|
||||
"ui.approval.outsideWorkspace": "(工作区之外:{{file}})",
|
||||
|
||||
"session.tab.review": "审查",
|
||||
"session.review.filesChanged": "{{count}} 个文件变更",
|
||||
@@ -472,44 +473,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "显示名称",
|
||||
"provider.custom.models.reasoning.label": "推理",
|
||||
"provider.custom.models.modalities.image": "图片",
|
||||
"provider.custom.models.variants.label": "变体",
|
||||
"provider.custom.models.variants.add": "添加变体",
|
||||
"provider.custom.models.variants.remove": "移除变体",
|
||||
"provider.custom.models.variants.name.label": "名称",
|
||||
"provider.custom.models.variants.name.placeholder": "例如 thinking",
|
||||
"provider.custom.models.variants.option.unset": "(未设置)",
|
||||
"provider.custom.models.variants.enableThinking.label": "启用思考 (例如 Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "思考类型 (例如 Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label": "通过聊天模板参数启用思考(如 Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "推理强度",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "移除模型",
|
||||
"provider.custom.models.add": "添加模型",
|
||||
"provider.custom.models.fetch.authError": "认证失败。请检查上方的 API 密钥后重试。",
|
||||
@@ -523,6 +486,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "搜索模型\u2026",
|
||||
"provider.custom.models.fetch.add": "添加 {{count}} 个模型",
|
||||
"provider.custom.edit.title": "编辑提供商",
|
||||
"provider.custom.edit.advanced": "在 JSON 配置文件中编辑高级设置",
|
||||
"provider.custom.headers.label": "请求头(可选)",
|
||||
"provider.custom.headers.key.label": "请求头",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
+2
-38
@@ -290,6 +290,7 @@ export const dict = {
|
||||
"ui.approval.source.yolo": "由自動核准(YOLO)模式",
|
||||
"ui.approval.source.session": "由工作階段自動核准規則",
|
||||
"ui.approval.source.default": "預設",
|
||||
"ui.approval.outsideWorkspace": "(工作區之外:{{file}})",
|
||||
|
||||
"session.tab.review": "審查",
|
||||
"session.review.filesChanged": "{{count}} 個檔案變更",
|
||||
@@ -432,44 +433,6 @@ export const dict = {
|
||||
"provider.custom.models.name.placeholder": "顯示名稱",
|
||||
"provider.custom.models.reasoning.label": "推理",
|
||||
"provider.custom.models.modalities.image": "圖片",
|
||||
"provider.custom.models.variants.label": "變體",
|
||||
"provider.custom.models.variants.add": "新增變體",
|
||||
"provider.custom.models.variants.remove": "移除變體",
|
||||
"provider.custom.models.variants.name.label": "名稱",
|
||||
"provider.custom.models.variants.name.placeholder": "例如 thinking",
|
||||
"provider.custom.models.variants.option.unset": "(未設定)",
|
||||
"provider.custom.models.variants.enableThinking.label": "啟用思考 (例如 Alibaba)",
|
||||
"provider.custom.models.variants.enableThinking.placeholder": "enable_thinking",
|
||||
"provider.custom.models.variants.enableThinking.true": "true",
|
||||
"provider.custom.models.variants.enableThinking.false": "false",
|
||||
"provider.custom.models.variants.thinking.label": "思考類型 (例如 Z.ai)",
|
||||
"provider.custom.models.variants.thinking.placeholder": "thinking",
|
||||
"provider.custom.models.variants.thinking.enabled": "enabled",
|
||||
"provider.custom.models.variants.thinking.disabled": "disabled",
|
||||
"provider.custom.models.variants.thinking.adaptive": "adaptive",
|
||||
"provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)",
|
||||
"provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split",
|
||||
"provider.custom.models.variants.splitReasoning.true": "true",
|
||||
"provider.custom.models.variants.splitReasoning.false": "false",
|
||||
"provider.custom.models.variants.chatTemplateArgs.label": "透過聊天範本參數啟用思考(如 Hugging Face)",
|
||||
"provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args",
|
||||
"provider.custom.models.variants.chatTemplateArgs.true": "true",
|
||||
"provider.custom.models.variants.chatTemplateArgs.false": "false",
|
||||
"provider.custom.models.variants.reasoningEffort.label": "推理強度",
|
||||
"provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort",
|
||||
"provider.custom.models.variants.reasoningEffort.none": "none",
|
||||
"provider.custom.models.variants.reasoningEffort.minimal": "minimal",
|
||||
"provider.custom.models.variants.reasoningEffort.low": "low",
|
||||
"provider.custom.models.variants.reasoningEffort.medium": "medium",
|
||||
"provider.custom.models.variants.reasoningEffort.high": "high",
|
||||
"provider.custom.models.variants.reasoningEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)",
|
||||
"provider.custom.models.variants.outputEffort.placeholder": "effort",
|
||||
"provider.custom.models.variants.outputEffort.low": "low",
|
||||
"provider.custom.models.variants.outputEffort.medium": "medium",
|
||||
"provider.custom.models.variants.outputEffort.high": "high",
|
||||
"provider.custom.models.variants.outputEffort.xhigh": "xhigh",
|
||||
"provider.custom.models.variants.outputEffort.max": "max",
|
||||
"provider.custom.models.remove": "移除模型",
|
||||
"provider.custom.models.add": "新增模型",
|
||||
"provider.custom.models.fetch.authError": "驗證失敗。請檢查上方的 API 金鑰後重試。",
|
||||
@@ -483,6 +446,7 @@ export const dict = {
|
||||
"provider.custom.models.fetch.search": "搜尋模型\u2026",
|
||||
"provider.custom.models.fetch.add": "新增 {{count}} 個模型",
|
||||
"provider.custom.edit.title": "編輯供應商",
|
||||
"provider.custom.edit.advanced": "在 JSON 設定檔中編輯進階設定",
|
||||
"provider.custom.headers.label": "標頭(選填)",
|
||||
"provider.custom.headers.key.label": "標頭",
|
||||
"provider.custom.headers.key.placeholder": "Header-Name",
|
||||
|
||||
@@ -72,7 +72,18 @@ const ACCESSIBLE_MODELS: EnrichedModel[] = [
|
||||
},
|
||||
autoRouting: { models: ["google/gemini-2.5-flash", "anthropic/claude-sonnet-4.6"] },
|
||||
},
|
||||
{ id: "omega", name: "Omega", providerID: "openai", providerName: "OpenAI", recommendedIndex: 1 },
|
||||
{
|
||||
id: "kilo-auto/frontier",
|
||||
name: "Kilo Auto Frontier",
|
||||
providerID: "kilo",
|
||||
providerName: "Kilo",
|
||||
recommendedIndex: 1,
|
||||
options: {
|
||||
description: "Routes each request to the strongest available models.",
|
||||
},
|
||||
autoRouting: { models: ["openai/gpt-5.5", "anthropic/claude-opus-4.6"] },
|
||||
},
|
||||
{ id: "omega", name: "Omega", providerID: "openai", providerName: "OpenAI", recommendedIndex: 2 },
|
||||
{ id: "alpha", name: "Alpha", providerID: "kilo", providerName: "Kilo" },
|
||||
{ id: "bravo", name: "Bravo", providerID: "kilo", providerName: "Kilo" },
|
||||
{ id: "charlie", name: "Charlie", providerID: "kilo", providerName: "Kilo" },
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { LanguageContextValue } from "../context/language"
|
||||
import type { OpenConfigFileRequest } from "../types/messages"
|
||||
|
||||
export function configMessage(scope: "local" | "global", t: LanguageContextValue["t"]): OpenConfigFileRequest {
|
||||
const label = t(scope === "global" ? "settings.config.scope.global" : "settings.config.scope.local")
|
||||
return {
|
||||
type: "openConfigFile",
|
||||
scope,
|
||||
labels: {
|
||||
scope: label,
|
||||
statusLoaded: t("settings.config.status.loaded"),
|
||||
statusLoadedLegacy: t("settings.config.status.loadedLegacy"),
|
||||
statusNotLoaded: t("settings.config.status.notLoaded"),
|
||||
statusCreate: t("settings.config.status.create"),
|
||||
title: t("settings.config.title", { scope: label }),
|
||||
placeholder: t("settings.config.placeholder"),
|
||||
noWorkspace: t("settings.config.noWorkspace"),
|
||||
openFailed: t("settings.config.openFailed", { scope: label, message: "{{message}}" }),
|
||||
sourceXdg: t("settings.config.source.xdg"),
|
||||
sourceHomeKilo: t("settings.config.source.homeKilo"),
|
||||
sourceHomeKilocode: t("settings.config.source.homeKilocode"),
|
||||
sourceHomeOpencode: t("settings.config.source.homeOpencode"),
|
||||
sourceEnvFile: t("settings.config.source.envFile"),
|
||||
sourceEnvDir: t("settings.config.source.envDir"),
|
||||
sourceEnvContent: t("settings.config.source.envContent"),
|
||||
sourceProjectKilo: t("settings.config.source.projectKilo"),
|
||||
sourceProjectRoot: t("settings.config.source.projectRoot"),
|
||||
sourceProjectKilocode: t("settings.config.source.projectKilocode"),
|
||||
sourceProjectOpencode: t("settings.config.source.projectOpencode"),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -516,6 +516,13 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
action: "deny",
|
||||
pattern: "*",
|
||||
},
|
||||
// kilocode_change start
|
||||
{
|
||||
permission: "suggest",
|
||||
action: "deny",
|
||||
pattern: "*",
|
||||
},
|
||||
// kilocode_change end
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -472,7 +472,12 @@ export const RunCommand = effectCmd({
|
||||
action: "deny",
|
||||
pattern: "*",
|
||||
},
|
||||
// kilocode_change start - non-interactive runs cannot take over a terminal
|
||||
// kilocode_change start - non-interactive runs cannot answer suggestions or take over a terminal
|
||||
{
|
||||
permission: "suggest",
|
||||
action: "deny",
|
||||
pattern: "*",
|
||||
},
|
||||
{
|
||||
permission: "interactive_terminal",
|
||||
action: "deny",
|
||||
@@ -904,6 +909,16 @@ export const RunCommand = effectCmd({
|
||||
break
|
||||
}
|
||||
|
||||
// kilocode_change start - non-interactive runs dismiss suggestions so they don't block
|
||||
if (event.type === "suggestion.shown") {
|
||||
const suggestion = event.properties
|
||||
if (suggestion.sessionID === sessionID || KiloRunAuto.allowed(tracked, suggestion.sessionID)) {
|
||||
await client.suggestion.dismiss({ requestID: suggestion.id }).catch(() => {})
|
||||
}
|
||||
continue
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
if (event.type === "permission.asked") {
|
||||
const permission = event.properties
|
||||
// kilocode_change start - skill shell batches need an interactive human decision. The server ignores
|
||||
|
||||
@@ -345,9 +345,7 @@ export namespace RemoteSender {
|
||||
// bus listener count from inflating for senders that never handle
|
||||
// attachments (the count would otherwise show up in unrelated tests
|
||||
// that assert it stays at 0).
|
||||
const attachments =
|
||||
options.attachments ??
|
||||
((sessionID: SessionID) => RemoteAttachments.create({ sessionID }))
|
||||
const attachments = options.attachments ?? ((sessionID: SessionID) => RemoteAttachments.create({ sessionID }))
|
||||
const attachmentCache = new Map<SessionID, RemoteAttachments.Result>()
|
||||
const pending = new Map<SessionID, number>()
|
||||
const retired = new Map<SessionID, RemoteAttachments.Result>()
|
||||
@@ -886,27 +884,27 @@ export namespace RemoteSender {
|
||||
return
|
||||
}
|
||||
// kilocode_change end
|
||||
// kilocode_change start - sessionless list_models for the pre-session instance picker
|
||||
if (msg.command === "list_models") {
|
||||
const parsed = RemoteModelCatalog.Request.safeParse(msg.data)
|
||||
const session = msg.sessionId ? decodeSessionID(msg.sessionId) : Option.none<SessionID>()
|
||||
if (!parsed.success || Option.isNone(session)) {
|
||||
options.conn.send({
|
||||
type: "response",
|
||||
id: msg.id,
|
||||
error: "invalid list_models command",
|
||||
})
|
||||
// Accept an absent sessionId (the mobile instance-picker path asks for the
|
||||
// instance's catalog before a session exists). A present but undecodable
|
||||
// sessionId is still invalid.
|
||||
const target = msg.sessionId ? decodeSessionID(msg.sessionId) : Option.none<SessionID>()
|
||||
if (!parsed.success || (msg.sessionId && Option.isNone(target))) {
|
||||
options.conn.send({ type: "response", id: msg.id, error: "invalid list_models command" })
|
||||
return
|
||||
}
|
||||
const run = options.provide ?? provide
|
||||
void (async () => {
|
||||
try {
|
||||
const info = await catalog.get(session.value)
|
||||
const info = Option.isSome(target) ? await catalog.get(target.value) : undefined
|
||||
const result = await run({
|
||||
directory: info.directory,
|
||||
directory: info?.directory ?? options.directory,
|
||||
fn: async () => {
|
||||
const [providers, messages, fallback] = await Promise.all([
|
||||
catalog.providers(),
|
||||
catalog.messages(info.id),
|
||||
info ? catalog.messages(info.id) : Promise.resolve([]),
|
||||
catalog.default().catch((err) => {
|
||||
options.log.warn("default model lookup failed", { error: String(err) })
|
||||
return undefined
|
||||
@@ -914,7 +912,7 @@ export namespace RemoteSender {
|
||||
])
|
||||
return RemoteModelCatalog.build({
|
||||
providers,
|
||||
session: info,
|
||||
session: info ?? {},
|
||||
messages,
|
||||
defaultModel: fallback,
|
||||
})
|
||||
@@ -928,6 +926,7 @@ export namespace RemoteSender {
|
||||
})()
|
||||
return
|
||||
}
|
||||
// kilocode_change end
|
||||
if (msg.command === "send_message") {
|
||||
const parsed = getRemotePromptInput().safeParse(msg.data)
|
||||
if (!parsed.success) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TRANSIENT } from "@kilocode/kilo-memory/schema"
|
||||
|
||||
type Event = {
|
||||
on(type: "memory.error", fn: (event: MemoryEvent) => void): void | (() => void)
|
||||
}
|
||||
@@ -22,6 +24,7 @@ export namespace MemoryTuiEvents {
|
||||
}) {
|
||||
const handler = (event: MemoryEvent) => {
|
||||
if (event.properties.sessionID && event.properties.sessionID !== input.sessionID) return
|
||||
if (event.properties.reason === TRANSIENT) return
|
||||
const detail = event.properties.detail
|
||||
if (!detail || typeof detail !== "object") {
|
||||
input.toast.show({
|
||||
|
||||
@@ -8,6 +8,7 @@ interface Cost {
|
||||
}
|
||||
|
||||
export function fmtPrice(n: number): string {
|
||||
if (n < 0) return "—"
|
||||
if (n === 0) return "Free"
|
||||
if (n < 0.01) return `$${n.toFixed(4)}/1M`
|
||||
return `$${n.toFixed(2)}/1M`
|
||||
|
||||
@@ -27,8 +27,9 @@ export function ModelInfoPanel(props: Props) {
|
||||
const maxHeight = createMemo(() => Math.floor(dimensions().height / 2) - 3)
|
||||
|
||||
const cost = () => m().cost
|
||||
const cached = () => (cost() ? fmtCachedPrice(cost()) : null)
|
||||
const avg = () => (cost() ? avgPrice(cost()) : undefined)
|
||||
const hasPricing = () => m().cost?.input > 0 || m().cost?.output > 0
|
||||
const cached = () => (cost() && hasPricing() ? fmtCachedPrice(cost()) : null)
|
||||
const avg = () => (cost() && hasPricing() ? avgPrice(cost()) : undefined)
|
||||
const caps = () => m().capabilities
|
||||
const inputs = () => caps()?.input
|
||||
const outputs = () => caps()?.output
|
||||
@@ -99,25 +100,27 @@ export function ModelInfoPanel(props: Props) {
|
||||
</Show>
|
||||
<Show when={!m().isFree}>
|
||||
<box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.textMuted}>Input</text>
|
||||
<text fg={theme.text}>{m() ? fmtPrice(m().cost.input) : "—"}</text>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.textMuted}>Output</text>
|
||||
<text fg={theme.text}>{m() ? fmtPrice(m().cost.output) : "—"}</text>
|
||||
</box>
|
||||
<Show when={cached()}>
|
||||
<Show when={hasPricing()}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.textMuted}>Cached</text>
|
||||
<text fg={theme.text}>{cached()}</text>
|
||||
<text fg={theme.textMuted}>Input</text>
|
||||
<text fg={theme.text}>{m() ? fmtPrice(m().cost.input) : "—"}</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={avg() !== undefined}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.textMuted}>Avg Cost</text>
|
||||
<text fg={theme.text}>{m() ? fmtPrice(avg()!) : "—"}</text>
|
||||
<text fg={theme.textMuted}>Output</text>
|
||||
<text fg={theme.text}>{m() ? fmtPrice(m().cost.output) : "—"}</text>
|
||||
</box>
|
||||
<Show when={cached()}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.textMuted}>Cached</text>
|
||||
<text fg={theme.text}>{cached()}</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={avg() !== undefined}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.textMuted}>Avg Cost</text>
|
||||
<text fg={theme.text}>{m() ? fmtPrice(avg()!) : "—"}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</Show>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.textMuted}>Context</text>
|
||||
|
||||
@@ -191,6 +191,7 @@ async function memoryText(input: {
|
||||
temperature: input.temperature,
|
||||
topP: input.topP,
|
||||
topK: input.topK,
|
||||
maxRetries: 1,
|
||||
}
|
||||
const work = async () => {
|
||||
if (!openai) return generateText(common)
|
||||
@@ -210,7 +211,7 @@ async function memoryText(input: {
|
||||
const timeout = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
ctl.abort()
|
||||
reject(new Error("memory model timed out"))
|
||||
reject(new DOMException("memory model timed out", "TimeoutError"))
|
||||
}, ms)
|
||||
})
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,21 @@ export namespace PermissionProvenance {
|
||||
agent?: string
|
||||
/** The winning rule, omitted for manual replies and the ask fallback. */
|
||||
rule?: { permission: string; pattern: string; action: Permission.Action }
|
||||
/** True when the ask's target path was outside the workspace/worktree (an `external_directory` ask). */
|
||||
outsideWorkspace?: boolean
|
||||
/** The target file path, when the `external_directory` ask carried one, for display as a filename. */
|
||||
outsideWorkspacePath?: string
|
||||
}
|
||||
|
||||
/** The `filepath` an `external_directory` ask's metadata carries, if any (see `Tool.assertExternalDirectory`). */
|
||||
export function filepathOf(metadata: Record<string, unknown> | undefined): string | undefined {
|
||||
return typeof metadata?.filepath === "string" ? metadata.filepath : undefined
|
||||
}
|
||||
|
||||
/** Tag an approval as outside-workspace when it answers an `external_directory` ask. */
|
||||
export function tagOutsideWorkspace(approval: Approval, permission: string, path?: string): Approval {
|
||||
if (permission !== "external_directory") return approval
|
||||
return { ...approval, outsideWorkspace: true, ...(path ? { outsideWorkspacePath: path } : {}) }
|
||||
}
|
||||
|
||||
export type Scope = "global" | "local"
|
||||
@@ -71,13 +86,29 @@ export namespace PermissionProvenance {
|
||||
* The approval is written once during `ask()`, but tools freely overwrite `state.metadata`
|
||||
* during execution and on completion. Carry the prior `approval` onto the replacement unless
|
||||
* the replacement sets its own.
|
||||
*
|
||||
* A file tool that crosses the workspace boundary issues *two* asks for one call: the generic
|
||||
* `external_directory` ask first, then its own `read`/`write`/`edit` ask. Both write `approval`
|
||||
* metadata, so the second ask's `outsideWorkspace` marker would otherwise clobber the first's
|
||||
* even though `"approval" in next` is true. Merge that marker forward so it survives.
|
||||
*/
|
||||
export function carryApproval(
|
||||
prev: Record<string, unknown> | undefined,
|
||||
next: Record<string, unknown> | undefined,
|
||||
) {
|
||||
if (!next || !prev?.approval || "approval" in next) return next
|
||||
return { ...next, approval: prev.approval }
|
||||
if (!next) return next
|
||||
const prior = prev?.approval as Approval | undefined
|
||||
if (!("approval" in next)) return prior ? { ...next, approval: prior } : next
|
||||
const current = next.approval as Approval | undefined
|
||||
if (!prior?.outsideWorkspace || !current || current.outsideWorkspace) return next
|
||||
return {
|
||||
...next,
|
||||
approval: {
|
||||
...current,
|
||||
outsideWorkspace: true,
|
||||
...(prior.outsideWorkspacePath ? { outsideWorkspacePath: prior.outsideWorkspacePath } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,8 @@ import { ProviderError } from "@/provider/error"
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { mapValues, omit, pickBy } from "remeda"
|
||||
import { reasoningSummary } from "./reasoning-summary"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
|
||||
/** Default timeout (ms) for provider HTTP requests (connection phase). */
|
||||
export const REQUEST_TIMEOUT_MS = 300_000 // 5 minutes
|
||||
@@ -95,6 +97,39 @@ export function patchConfigModel(cfg: any, existing: any) {
|
||||
}
|
||||
}
|
||||
|
||||
const CUSTOM_PROVIDER_PACKAGES = new Set(["@ai-sdk/openai-compatible", "@ai-sdk/openai", "@ai-sdk/anthropic"])
|
||||
const FALLBACK_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"]
|
||||
type Variants = NonNullable<Provider.Model["variants"]>
|
||||
type Generate = (model: Provider.Model) => Variants
|
||||
|
||||
export function customProviderVariants(model: Provider.Model, npm: unknown, generate: Generate): Variants {
|
||||
if (model.variants && Object.keys(model.variants).length > 0) return model.variants
|
||||
|
||||
const supported = typeof npm === "string" && CUSTOM_PROVIDER_PACKAGES.has(npm) && model.api.npm === npm
|
||||
const variants = generate(model)
|
||||
if (Object.keys(variants).length > 0) return variants
|
||||
if (!model.capabilities.reasoning || !supported) return variants
|
||||
|
||||
return Object.fromEntries(
|
||||
FALLBACK_EFFORTS.map((effort) => {
|
||||
if (npm === "@ai-sdk/anthropic") {
|
||||
return [effort, effort === "none" ? { thinking: { type: "disabled" } } : { effort }]
|
||||
}
|
||||
if (npm === "@ai-sdk/openai") {
|
||||
return [
|
||||
effort,
|
||||
{
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: reasoningSummary(model),
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
]
|
||||
}
|
||||
return [effort, { reasoningEffort: effort }]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom loaders (new or fully-replaced loaders)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -80,6 +80,7 @@ export namespace KiloTask {
|
||||
return [
|
||||
{ permission: "task", pattern: "*", action: "deny" },
|
||||
{ permission: "question", pattern: "*", action: "deny" },
|
||||
{ permission: "suggest", pattern: "*", action: "deny" },
|
||||
{ permission: "interactive_terminal", pattern: "*", action: "deny" },
|
||||
...rules,
|
||||
]
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
KILO_MODEL_SCHEMA_EXTENSIONS,
|
||||
patchModelsDevModel as patchKiloModel,
|
||||
patchConfigModel as patchKiloConfigModel,
|
||||
customProviderVariants,
|
||||
patchCustomLoaderResult,
|
||||
patchKiloProviderPrivacy,
|
||||
kiloSmallModelPriority,
|
||||
@@ -1526,7 +1527,12 @@ const layer = Layer.effect(
|
||||
// variants: {}, // kilocode_change, moved into patchKiloConfigModel
|
||||
...patchKiloConfigModel(model, existingModel), // kilocode_change
|
||||
}
|
||||
const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
|
||||
// kilocode_change start
|
||||
const generated = Object.keys(model.variants ?? {}).length
|
||||
? {}
|
||||
: customProviderVariants(parsedModel, model.provider?.npm ?? provider.npm, ProviderTransform.variants)
|
||||
const merged = mergeDeep(generated, model.variants ?? {})
|
||||
// kilocode_change end
|
||||
parsedModel.variants = mapValues(
|
||||
pickBy(merged, (v): v is NonNullable<typeof v> => !!v && !v.disabled), // kilocode_change - drop null delete sentinels
|
||||
(v) => omit(v, ["disabled"]),
|
||||
|
||||
@@ -328,6 +328,20 @@ function normalizeMessages(
|
||||
return msgs
|
||||
}
|
||||
|
||||
// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+
|
||||
function supportsPromptCacheBreakpoint(modelId: string): boolean {
|
||||
const match = modelId.match(/gpt-(\d+)\.(\d+)/)
|
||||
if (match) {
|
||||
const major = Number(match[1])
|
||||
const minor = Number(match[2])
|
||||
if (major > 5 || (major === 5 && minor >= 6)) return true
|
||||
}
|
||||
const majorMatch = modelId.match(/gpt-(\d+)/)
|
||||
if (majorMatch && Number(majorMatch[1]) >= 6) return true
|
||||
return false
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
@@ -351,6 +365,18 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage
|
||||
alibaba: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
// kilocode_change start
|
||||
...(supportsPromptCacheBreakpoint(model.api.id)
|
||||
? {
|
||||
openai: {
|
||||
promptCacheBreakpoint: { mode: "explicit" },
|
||||
},
|
||||
azure: {
|
||||
promptCacheBreakpoint: { mode: "explicit" },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
@@ -360,8 +386,24 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage
|
||||
model.api.npm === "@ai-sdk/amazon-bedrock"
|
||||
const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
|
||||
|
||||
if (shouldUseContentOptions) {
|
||||
const lastContent = msg.content[msg.content.length - 1]
|
||||
// kilocode_change start - place caching breakpoint on stable content before trailing <environment_details>
|
||||
if (shouldUseContentOptions && Array.isArray(msg.content)) {
|
||||
const parts = msg.content
|
||||
let targetIndex = -1
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
const part = parts[i]
|
||||
if (
|
||||
part &&
|
||||
typeof part === "object" &&
|
||||
part.type !== "tool-approval-request" &&
|
||||
part.type !== "tool-approval-response" &&
|
||||
!(part.type === "text" && part.text.startsWith("<environment_details>"))
|
||||
) {
|
||||
targetIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
const lastContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1]
|
||||
if (
|
||||
lastContent &&
|
||||
typeof lastContent === "object" &&
|
||||
@@ -372,6 +414,7 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage
|
||||
continue
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
|
||||
}
|
||||
@@ -438,6 +481,7 @@ function mapProviderOptions(
|
||||
export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
|
||||
msgs = unsupportedParts(msgs, model)
|
||||
msgs = normalizeMessages(msgs, model, options)
|
||||
// kilocode_change start - apply caching for anthropic, alibaba, and GPT-5.6+ openai/azure/kilo-gateway
|
||||
if (
|
||||
(model.providerID === "anthropic" ||
|
||||
model.providerID === "google-vertex-anthropic" ||
|
||||
@@ -446,11 +490,16 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re
|
||||
model.id.includes("anthropic") ||
|
||||
model.id.includes("claude") ||
|
||||
model.api.npm === "@ai-sdk/anthropic" ||
|
||||
model.api.npm === "@ai-sdk/alibaba") &&
|
||||
model.api.npm === "@ai-sdk/alibaba" ||
|
||||
((model.api.npm === "@ai-sdk/openai" ||
|
||||
model.api.npm === "@ai-sdk/azure" ||
|
||||
model.api.npm === "@kilocode/kilo-gateway") &&
|
||||
supportsPromptCacheBreakpoint(model.api.id))) &&
|
||||
model.api.npm !== "@ai-sdk/gateway"
|
||||
) {
|
||||
msgs = applyCaching(msgs, model)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// Remap providerOptions keys from stored providerID to expected SDK key
|
||||
const key = sdkKey(model.api.npm)
|
||||
|
||||
@@ -97,18 +97,32 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||
},
|
||||
}).pipe(
|
||||
// record why the call was allowed onto the tool part, then discard the outcome for the tool-facing ask
|
||||
Effect.tap((approval) => input.processor.metadata(options.toolCallId, { metadata: { approval } })),
|
||||
Effect.tap((approval) =>
|
||||
input.processor.metadata(options.toolCallId, {
|
||||
metadata: {
|
||||
approval: PermissionProvenance.tagOutsideWorkspace(
|
||||
approval,
|
||||
req.permission,
|
||||
PermissionProvenance.filepathOf(req.metadata),
|
||||
),
|
||||
},
|
||||
}),
|
||||
),
|
||||
// record why the call was denied too, so JSON exports and clients can explain the denial
|
||||
Effect.tapErrorTag("PermissionDeniedError", (err) =>
|
||||
input.processor.metadata(options.toolCallId, {
|
||||
metadata: {
|
||||
approval: PermissionProvenance.classifyDenial({
|
||||
ruleset: err.ruleset,
|
||||
permission: req.permission,
|
||||
patterns: req.patterns,
|
||||
agent: input.agent.name,
|
||||
origins: permissionOrigins,
|
||||
}),
|
||||
approval: PermissionProvenance.tagOutsideWorkspace(
|
||||
PermissionProvenance.classifyDenial({
|
||||
ruleset: err.ruleset,
|
||||
permission: req.permission,
|
||||
patterns: req.patterns,
|
||||
agent: input.agent.name,
|
||||
origins: permissionOrigins,
|
||||
}),
|
||||
req.permission,
|
||||
PermissionProvenance.filepathOf(req.metadata),
|
||||
),
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -18,6 +18,11 @@ describe("Auto mode flag", () => {
|
||||
action: "deny" as const,
|
||||
pattern: "*",
|
||||
},
|
||||
{
|
||||
permission: "suggest",
|
||||
action: "deny" as const,
|
||||
pattern: "*",
|
||||
},
|
||||
{
|
||||
permission: "interactive_terminal",
|
||||
action: "deny" as const,
|
||||
@@ -25,7 +30,7 @@ describe("Auto mode flag", () => {
|
||||
},
|
||||
]
|
||||
|
||||
expect(autoPermissions).toHaveLength(3)
|
||||
expect(autoPermissions).toHaveLength(4)
|
||||
|
||||
// First rule: allow all
|
||||
expect(autoPermissions[0].permission).toBe("*")
|
||||
@@ -36,9 +41,12 @@ describe("Auto mode flag", () => {
|
||||
expect(autoPermissions[1].permission).toBe("question")
|
||||
expect(autoPermissions[1].action).toBe("deny")
|
||||
expect(autoPermissions[1].pattern).toBe("*")
|
||||
expect(autoPermissions[2].permission).toBe("interactive_terminal")
|
||||
expect(autoPermissions[2].permission).toBe("suggest")
|
||||
expect(autoPermissions[2].action).toBe("deny")
|
||||
expect(autoPermissions[2].pattern).toBe("*")
|
||||
expect(autoPermissions[3].permission).toBe("interactive_terminal")
|
||||
expect(autoPermissions[3].action).toBe("deny")
|
||||
expect(autoPermissions[3].pattern).toBe("*")
|
||||
})
|
||||
|
||||
test("non-auto mode should not set allow-all permissions", () => {
|
||||
@@ -55,6 +63,7 @@ describe("Auto mode flag", () => {
|
||||
const autoPermissions = [
|
||||
{ permission: "*", action: "allow" as const, pattern: "*" },
|
||||
{ permission: "question", action: "deny" as const, pattern: "*" },
|
||||
{ permission: "suggest", action: "deny" as const, pattern: "*" },
|
||||
{ permission: "interactive_terminal", action: "deny" as const, pattern: "*" },
|
||||
]
|
||||
|
||||
@@ -71,6 +80,8 @@ describe("Auto mode flag", () => {
|
||||
// Test that "question" permission resolves to "deny"
|
||||
const questionRule = findLastMatch("question")
|
||||
expect(questionRule?.action).toBe("deny")
|
||||
const suggestRule = findLastMatch("suggest")
|
||||
expect(suggestRule?.action).toBe("deny")
|
||||
const terminalRule = findLastMatch("interactive_terminal")
|
||||
expect(terminalRule?.action).toBe("deny")
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// `KILO_CONFIG_CONTENT` providing the test provider config inline.
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { createKiloClient } from "@kilocode/sdk/v2" // kilocode_change
|
||||
import { reply } from "../../lib/llm-server"
|
||||
import { cliIt } from "../../lib/cli-process"
|
||||
|
||||
@@ -412,4 +413,60 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
}),
|
||||
60_000, // kilocode_change
|
||||
)
|
||||
|
||||
// kilocode_change start - non-interactive runs exclude human-driven tools like suggest
|
||||
cliIt.concurrent(
|
||||
"kilo run --auto excludes suggest tool from LLM request",
|
||||
({ llm, opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
yield* llm.text("done")
|
||||
const result = yield* opencode.run("do work", { extraArgs: ["--auto"] })
|
||||
opencode.expectExit(result, 0)
|
||||
expect(result.stdout).toBe("done\n")
|
||||
const inputs = yield* llm.inputs
|
||||
const tools = (inputs[0]?.body as any)?.tools as Array<{ function?: { name?: string } }> | undefined
|
||||
const toolNames = tools?.map((t) => t.function?.name).filter(Boolean) ?? []
|
||||
expect(toolNames).not.toContain("suggest")
|
||||
expect(toolNames).not.toContain("question")
|
||||
expect(toolNames).not.toContain("interactive_terminal")
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
|
||||
cliIt.live(
|
||||
"kilo run auto-dismisses suggestion and exits cleanly if suggest tool is invoked in attached session",
|
||||
({ llm, opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const server = yield* opencode.serve()
|
||||
const client = createKiloClient({ baseUrl: server.url })
|
||||
const session = yield* Effect.promise(() =>
|
||||
client.session.create({
|
||||
permission: [{ permission: "suggest", action: "allow", pattern: "*" }],
|
||||
}),
|
||||
)
|
||||
const sessionID = session.data?.id
|
||||
expect(sessionID).toBeDefined()
|
||||
|
||||
yield* llm.push(
|
||||
reply().tool("suggest", {
|
||||
suggest: "Run checks?",
|
||||
actions: [{ label: "Run checks", prompt: "Run checks" }],
|
||||
}),
|
||||
)
|
||||
yield* llm.text("completed after dismissal")
|
||||
|
||||
const result = yield* opencode.run("do work", {
|
||||
extraArgs: ["--attach", server.url, "--session", sessionID!, "--auto"],
|
||||
})
|
||||
opencode.expectExit(result, 0)
|
||||
|
||||
const messages = yield* Effect.promise(() => client.session.messages({ sessionID: sessionID! }))
|
||||
const assistant = messages.data?.findLast((m) => m.info.role === "assistant")
|
||||
const toolPart = assistant?.parts.find((p) => p.type === "tool" && p.tool === "suggest")
|
||||
expect(toolPart).toBeDefined()
|
||||
expect((toolPart as any)?.state?.metadata?.dismissed).toBe(true)
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
// kilocode_change end
|
||||
})
|
||||
|
||||
@@ -276,7 +276,7 @@ describe("memory TUI events", () => {
|
||||
expect(handlers).toEqual({ "memory.error": [expect.any(Function)] })
|
||||
})
|
||||
|
||||
test("keeps generic and detailed errors visible", async () => {
|
||||
test("suppresses transient errors while keeping generic and detailed errors visible", async () => {
|
||||
const shown: string[] = []
|
||||
const handlers: Record<string, Handler[]> = {}
|
||||
MemoryTuiEvents.attach({
|
||||
@@ -298,6 +298,17 @@ describe("memory TUI events", () => {
|
||||
fn({ properties: { sessionID: "ses_tui_memory", reason: "model failed" } }),
|
||||
),
|
||||
)
|
||||
await Promise.all(
|
||||
(handlers["memory.error"] ?? []).map((fn) =>
|
||||
fn({
|
||||
properties: {
|
||||
sessionID: "ses_tui_memory",
|
||||
reason: "transient",
|
||||
detail: { message: "Memory model timed out" },
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
await Promise.all(
|
||||
(handlers["memory.error"] ?? []).map((fn) =>
|
||||
fn({ properties: { sessionID: "ses_tui_memory", detail: { message: "Memory save failed" } } }),
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { expect } from "bun:test"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Effect } from "effect"
|
||||
import { Env } from "@/env"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node])))
|
||||
|
||||
it.instance(
|
||||
"uses configured variants instead of inferred reasoning efforts",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.use.list()
|
||||
const model = providers[ProviderV2.ID.make("custom")]?.models["qwen-custom"]
|
||||
|
||||
expect(Object.keys(model?.variants ?? {})).toEqual(["custom"])
|
||||
expect(model?.variants?.high).toBeUndefined()
|
||||
expect(model?.variants?.custom).toEqual({ reasoningEffort: "custom" })
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
custom: {
|
||||
name: "Custom",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
options: { apiKey: "test" },
|
||||
models: {
|
||||
"qwen-custom": {
|
||||
name: "Qwen Custom",
|
||||
reasoning: true,
|
||||
limit: { context: 128_000, output: 16_000 },
|
||||
variants: {
|
||||
high: { disabled: true },
|
||||
custom: { reasoningEffort: "custom" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { APICallError } from "ai"
|
||||
import { Effect } from "effect"
|
||||
import { ModelNotFoundError, type Provider } from "../../../src/provider/provider"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
@@ -31,15 +32,22 @@ function mdl(id = mid): Provider.Model {
|
||||
} as unknown as Provider.Model
|
||||
}
|
||||
|
||||
function lang(outputs = ["{}"]): LanguageModelV3 {
|
||||
function lang(outputs: (string | Error)[] = ["{}"], calls?: unknown[], hang?: boolean): LanguageModelV3 {
|
||||
let idx = 0
|
||||
const next = () => {
|
||||
const item = outputs[idx++] ?? outputs.at(-1) ?? "{}"
|
||||
if (item instanceof Error) throw item
|
||||
return item
|
||||
}
|
||||
return {
|
||||
specificationVersion: "v3",
|
||||
provider: "test",
|
||||
modelId: "fake-memory-model",
|
||||
supportedUrls: {},
|
||||
doGenerate: async () => {
|
||||
const text = outputs[idx++] ?? outputs.at(-1) ?? "{}"
|
||||
doGenerate: async (...args: Parameters<LanguageModelV3["doGenerate"]>) => {
|
||||
calls?.push(args[0])
|
||||
if (hang) return new Promise(() => {})
|
||||
const text = next()
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
finishReason: { unified: "stop" },
|
||||
@@ -57,7 +65,9 @@ function lang(outputs = ["{}"]): LanguageModelV3 {
|
||||
} as unknown as LanguageModelV3
|
||||
}
|
||||
|
||||
function provider(input: { outputs?: string[]; seen?: string[] } = {}): Provider.Interface {
|
||||
function provider(
|
||||
input: { outputs?: (string | Error)[]; seen?: string[]; calls?: unknown[]; hang?: boolean } = {},
|
||||
): Provider.Interface {
|
||||
const base = mdl()
|
||||
const mem = mdl(ModelV2.ID.make("memory-config-model"))
|
||||
const info = {
|
||||
@@ -78,7 +88,7 @@ function provider(input: { outputs?: string[]; seen?: string[] } = {}): Provider
|
||||
},
|
||||
getLanguage: (model) => {
|
||||
input.seen?.push(model.id)
|
||||
return Effect.succeed(lang(input.outputs))
|
||||
return Effect.succeed(lang(input.outputs, input.calls, input.hang))
|
||||
},
|
||||
closest: () => Effect.succeed({ providerID: pid, modelID: base.id }),
|
||||
getSmallModel: () => Effect.succeed(mem),
|
||||
@@ -280,6 +290,34 @@ describe("memory ports", () => {
|
||||
expect(seen).toEqual(["memory-config-model", "fake-memory-model"])
|
||||
})
|
||||
|
||||
test("model port retries a transient provider failure once", async () => {
|
||||
const calls: unknown[] = []
|
||||
const err = new APICallError({
|
||||
message: "temporarily unavailable",
|
||||
url: "https://example.com/v1/generate",
|
||||
requestBodyValues: {},
|
||||
statusCode: 503,
|
||||
responseHeaders: {},
|
||||
responseBody: '{"error":"temporarily unavailable"}',
|
||||
isRetryable: true,
|
||||
})
|
||||
const port = MemoryModel.port({ provider: provider({ outputs: [err, "{}"], calls }) })
|
||||
const resolved = await Effect.runPromise(port.resolve({ session: ref }))
|
||||
|
||||
await port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 30_000 })
|
||||
|
||||
expect(calls).toHaveLength(2)
|
||||
})
|
||||
|
||||
test("model port emits a structured timeout error", async () => {
|
||||
const port = MemoryModel.port({ provider: provider({ hang: true }) })
|
||||
const resolved = await Effect.runPromise(port.resolve({ session: ref }))
|
||||
|
||||
await expect(
|
||||
port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 1 }),
|
||||
).rejects.toMatchObject({ name: "TimeoutError", message: "memory model timed out" })
|
||||
})
|
||||
|
||||
test("model port clears its timeout after successful output", async () => {
|
||||
const set = globalThis.setTimeout
|
||||
const clear = globalThis.clearTimeout
|
||||
|
||||
@@ -6,6 +6,11 @@ describe("model info panel price formatting", () => {
|
||||
expect(fmtPrice(0)).toBe("Free")
|
||||
})
|
||||
|
||||
test("fmtPrice returns dash for negative values", () => {
|
||||
expect(fmtPrice(-1)).toBe("—")
|
||||
expect(fmtPrice(-1000000)).toBe("—")
|
||||
})
|
||||
|
||||
test("fmtPrice uses four decimals for very small prices", () => {
|
||||
expect(fmtPrice(0.0095)).toBe("$0.0095/1M")
|
||||
})
|
||||
|
||||
@@ -121,6 +121,80 @@ describe("PermissionProvenance.carryApproval", () => {
|
||||
test("returns the replacement as-is when it is undefined", () => {
|
||||
expect(PermissionProvenance.carryApproval({ approval }, undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("merges outsideWorkspace onto a replacement's own approval instead of clobbering it", () => {
|
||||
// A file tool crossing the workspace boundary asks twice: external_directory first, then its
|
||||
// own read/write/edit ask. The second ask's approval must not lose the outsideWorkspace marker.
|
||||
const outside = { source: "manual" as const, outsideWorkspace: true }
|
||||
const next = { approval: { source: "agent" as const, agent: "build" } }
|
||||
expect(PermissionProvenance.carryApproval({ approval: outside }, next)).toEqual({
|
||||
approval: { source: "agent", agent: "build", outsideWorkspace: true },
|
||||
})
|
||||
})
|
||||
|
||||
test("also carries the outsideWorkspacePath forward alongside the marker", () => {
|
||||
const outside = { source: "manual" as const, outsideWorkspace: true, outsideWorkspacePath: "/tmp/secret.txt" }
|
||||
const next = { approval: { source: "agent" as const, agent: "build" } }
|
||||
expect(PermissionProvenance.carryApproval({ approval: outside }, next)).toEqual({
|
||||
approval: { source: "agent", agent: "build", outsideWorkspace: true, outsideWorkspacePath: "/tmp/secret.txt" },
|
||||
})
|
||||
})
|
||||
|
||||
test("does not add outsideWorkspace when the prior approval was not outside the workspace", () => {
|
||||
const next = { approval: { source: "agent" as const, agent: "build" } }
|
||||
expect(PermissionProvenance.carryApproval({ approval }, next)).toBe(next)
|
||||
})
|
||||
|
||||
test("leaves a replacement's own outsideWorkspace marker untouched", () => {
|
||||
const outside = { source: "manual" as const, outsideWorkspace: true }
|
||||
const next = { approval: { source: "agent" as const, agent: "build", outsideWorkspace: true } }
|
||||
expect(PermissionProvenance.carryApproval({ approval: outside }, next)).toBe(next)
|
||||
})
|
||||
})
|
||||
|
||||
describe("PermissionProvenance.tagOutsideWorkspace", () => {
|
||||
test("marks an external_directory approval as outsideWorkspace", () => {
|
||||
const approval = { source: "manual" as const }
|
||||
expect(PermissionProvenance.tagOutsideWorkspace(approval, "external_directory")).toEqual({
|
||||
source: "manual",
|
||||
outsideWorkspace: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("leaves other permissions' approvals untouched", () => {
|
||||
const approval = { source: "manual" as const }
|
||||
expect(PermissionProvenance.tagOutsideWorkspace(approval, "read")).toBe(approval)
|
||||
})
|
||||
|
||||
test("carries the target path when one is given", () => {
|
||||
const approval = { source: "manual" as const }
|
||||
expect(PermissionProvenance.tagOutsideWorkspace(approval, "external_directory", "/tmp/secret.txt")).toEqual({
|
||||
source: "manual",
|
||||
outsideWorkspace: true,
|
||||
outsideWorkspacePath: "/tmp/secret.txt",
|
||||
})
|
||||
})
|
||||
|
||||
test("omits outsideWorkspacePath when no path is given", () => {
|
||||
const approval = { source: "manual" as const }
|
||||
expect(PermissionProvenance.tagOutsideWorkspace(approval, "external_directory")).toEqual({
|
||||
source: "manual",
|
||||
outsideWorkspace: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("PermissionProvenance.filepathOf", () => {
|
||||
test("reads the filepath an external_directory ask's metadata carries", () => {
|
||||
expect(PermissionProvenance.filepathOf({ filepath: "/tmp/secret.txt", parentDir: "/tmp" })).toBe(
|
||||
"/tmp/secret.txt",
|
||||
)
|
||||
})
|
||||
|
||||
test("returns undefined when there is no filepath, e.g. a bash directory scan", () => {
|
||||
expect(PermissionProvenance.filepathOf({ command: "cat /tmp/secret.txt", access: "read" })).toBeUndefined()
|
||||
expect(PermissionProvenance.filepathOf(undefined)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("askPermission returns provenance", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ProviderTransform } from "../../src/provider/transform"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { customProviderVariants } from "../../src/kilocode/provider/provider"
|
||||
import type * as ModelsDev from "@opencode-ai/core/models-dev"
|
||||
|
||||
function mockModel(overrides: Partial<any> = {}): any {
|
||||
@@ -138,3 +139,67 @@ describe("ProviderTransform.reasoningVariants - models.dev reasoning_options", (
|
||||
expect(Object.keys(gpt5.variants ?? {})).toEqual(["minimal", "low", "medium", "high"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("custom provider fallback reasoning efforts", () => {
|
||||
const efforts = ["none", "low", "medium", "high", "xhigh", "max"]
|
||||
|
||||
for (const npm of ["@ai-sdk/openai-compatible", "@ai-sdk/openai", "@ai-sdk/anthropic"]) {
|
||||
test(`${npm} exposes broad efforts after heuristics fail`, () => {
|
||||
const model = mockModel({ id: "qwen-custom", api: { id: "qwen-custom", url: "https://api.test.com", npm } })
|
||||
const generated = ProviderTransform.variants({ ...model, variants: {} })
|
||||
expect(generated).toEqual({})
|
||||
|
||||
const result = customProviderVariants(model, npm, ProviderTransform.variants)
|
||||
|
||||
expect(Object.keys(result)).toEqual(efforts)
|
||||
if (npm === "@ai-sdk/anthropic") {
|
||||
expect(result.none).toEqual({ thinking: { type: "disabled" } })
|
||||
expect(result.max).toEqual({ effort: "max" })
|
||||
return
|
||||
}
|
||||
expect(result.none?.reasoningEffort).toBe("none")
|
||||
expect(result.max?.reasoningEffort).toBe("max")
|
||||
})
|
||||
}
|
||||
|
||||
test("preserves successful heuristics", () => {
|
||||
const model = mockModel({ api: { id: "custom", url: "https://api.test.com", npm: "@ai-sdk/openai-compatible" } })
|
||||
const generated = { low: { reasoningEffort: "low" }, high: { reasoningEffort: "high" } }
|
||||
expect(customProviderVariants(model, model.api.npm, () => generated)).toBe(generated)
|
||||
})
|
||||
|
||||
test("prefers configured variants to inference", () => {
|
||||
const variants = { custom: { reasoningEffort: "custom" } }
|
||||
for (const npm of ["@ai-sdk/openai-compatible", "@ai-sdk/openai", "@ai-sdk/anthropic"]) {
|
||||
const model = mockModel({ api: { id: "custom", url: "https://api.test.com", npm }, variants })
|
||||
expect(
|
||||
customProviderVariants(model, npm, () => {
|
||||
throw new Error("inference should not run")
|
||||
}),
|
||||
).toBe(variants)
|
||||
}
|
||||
})
|
||||
|
||||
test("requires a reasoning model with an explicitly configured supported package", () => {
|
||||
const npm = "@ai-sdk/openai-compatible"
|
||||
const plain = mockModel({
|
||||
api: { id: "custom", url: "https://api.test.com", npm },
|
||||
capabilities: { ...mockModel().capabilities, reasoning: false },
|
||||
})
|
||||
expect(customProviderVariants(plain, npm, () => ({}))).toEqual({})
|
||||
expect(
|
||||
customProviderVariants(
|
||||
mockModel({ api: { id: "custom", url: "https://api.test.com", npm } }),
|
||||
undefined,
|
||||
() => ({}),
|
||||
),
|
||||
).toEqual({})
|
||||
expect(
|
||||
customProviderVariants(
|
||||
mockModel({ api: { id: "custom", url: "https://api.test.com", npm: "unrelated-provider" } }),
|
||||
"unrelated-provider",
|
||||
() => ({}),
|
||||
),
|
||||
).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user