opencode-zen-freex 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +125 -3
- package/core-tools.mjs +53 -0
- package/fingerprint.json +241 -0
- package/index.mjs +3 -0
- package/models.mjs +148 -0
- package/package.json +26 -3
- package/provider.mjs +461 -0
- package/stream.mjs +398 -0
- package/tool-response.mjs +881 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xiaozhou26
|
|
4
|
+
Copyright (c) 2026 opencode-zen-free contributors
|
|
5
|
+
|
|
6
|
+
This is a maintained fork of the OpenCode Zen Free plugin from
|
|
7
|
+
magpie-community/plugins, which is MIT licensed and carries the notice above.
|
|
8
|
+
The fork is maintained separately because the free tier's request fingerprint
|
|
9
|
+
moves faster than that repository follows it.
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,125 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# OpenCode Zen Free
|
|
2
|
+
|
|
3
|
+
OpenCode Zen's free models for a host that is not OpenCode. Provider id:
|
|
4
|
+
`opencode-zen-free`.
|
|
5
|
+
|
|
6
|
+
Zen's free tier answers 403 FreeTierError ("can only be used from within
|
|
7
|
+
OpenCode") to a request that isn't OpenCode's, and it reads the request body
|
|
8
|
+
as well as its headers: the tools it knows by name have to be described the
|
|
9
|
+
way OpenCode describes them. This plugin asks the endpoint that serves the
|
|
10
|
+
free tier, and presents the tools the way OpenCode presents them while
|
|
11
|
+
keeping the host's own tools, so its calls still run.
|
|
12
|
+
|
|
13
|
+
This is a maintained fork of the plugin from `magpie-community/plugins`,
|
|
14
|
+
which is MIT licensed. It is forked because that fingerprint moves faster
|
|
15
|
+
than that repository follows it: its last release is still built for an
|
|
16
|
+
endpoint that no longer serves the free tier. See [MAINTAINING.md](MAINTAINING.md)
|
|
17
|
+
for how to keep it working, and `bun run check:gate` for whether it is
|
|
18
|
+
working now.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
magpie plugin add opencode-zen-freex
|
|
24
|
+
magpie plugin login opencode-zen-free
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Sign in
|
|
28
|
+
|
|
29
|
+
Enter `public` in the key field. No personal API key is needed.
|
|
30
|
+
The credential is kept in OpenCode's `auth.json` or magpie's
|
|
31
|
+
`plugin-auth.json`.
|
|
32
|
+
|
|
33
|
+
## Models
|
|
34
|
+
|
|
35
|
+
The plugin discovers Zen's current free models, including their context
|
|
36
|
+
windows and reasoning levels. Use `opencode-zen-free/<model>`, for example
|
|
37
|
+
`opencode-zen-free/big-pickle`.
|
|
38
|
+
|
|
39
|
+
Retired models and SystemOne models are excluded. The list is asked
|
|
40
|
+
again each time the host lists models (magpie does this on start and every
|
|
41
|
+
hour): a model Zen no longer gives free leaves it, and a new free one joins
|
|
42
|
+
it. When Zen's list can't be read, the last list is kept. A request for a
|
|
43
|
+
model that isn't free now gets a 404 naming the free ones; one made free
|
|
44
|
+
since the last list is served after the list is asked again (at most once a
|
|
45
|
+
minute). Availability and rate limits are set by Zen; the plugin has no
|
|
46
|
+
remaining-quota data.
|
|
47
|
+
|
|
48
|
+
## When the free tier stops answering
|
|
49
|
+
|
|
50
|
+
A stale fingerprint does not look different from a working one until
|
|
51
|
+
somebody is refused. Ask Zen which generations the plugin still speaks:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
bun run check:gate
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Each profile is reported as `answered`, `refused` (403), `superseded`
|
|
58
|
+
(426 — Zen wants a newer OpenCode than the profile claims) or
|
|
59
|
+
`unreachable`. Run it before a release and whenever a 403 is reported.
|
|
60
|
+
|
|
61
|
+
If every profile is refused, capture the wording again from a current
|
|
62
|
+
OpenCode: point a release at a local server and read the tools out of the
|
|
63
|
+
request it makes. A refusal from the plugin says which wording went out
|
|
64
|
+
and where to look, rather than leaving it to be guessed at.
|
|
65
|
+
|
|
66
|
+
Note that the gate is not steady: it moved more than once in a day while
|
|
67
|
+
this was being worked out, and repeated refusals from one address can
|
|
68
|
+
themselves be answered for a while. Read a verdict here as "at this
|
|
69
|
+
moment", and run it again before believing a change.
|
|
70
|
+
|
|
71
|
+
## Requests
|
|
72
|
+
|
|
73
|
+
Requests go to `https://opencode.ai/inference/v1` using each model's
|
|
74
|
+
native Chat Completions, Responses or Anthropic Messages API. Streaming and
|
|
75
|
+
ordinary JSON replies are supported.
|
|
76
|
+
|
|
77
|
+
The agent's tools are preserved. Zen's models often call OpenCode's own
|
|
78
|
+
tool names; such a call is given the agent's tool whose name differs only
|
|
79
|
+
in case (`bash` to `Bash`), or else only in case and `_`/`-` (`todowrite`
|
|
80
|
+
to `todo_write`, `webfetch` to `web_fetch`), when exactly one does. A
|
|
81
|
+
call with no such tool (or with no name) is left out of the reply, and
|
|
82
|
+
the reply's text says which name was left out; a turn whose every call
|
|
83
|
+
was left out ends as an ordinary answer. Tools run in the agent.
|
|
84
|
+
|
|
85
|
+
Zen's free tier reads the body as well as the headers, and answers a
|
|
86
|
+
request only when the whole of it belongs to one generation of OpenCode:
|
|
87
|
+
the User-Agent, the headers and the tools have to agree. Two generations
|
|
88
|
+
are current, and the tools a request carries say which: `shell` and `read`
|
|
89
|
+
are 2.x, `bash`, `glob`, `grep` and `read` are 1.x. A host that is OpenCode
|
|
90
|
+
itself says which it is in its own User-Agent, and that is passed on rather
|
|
91
|
+
than pinned, so it is never behind. Where nothing says, the tools decide
|
|
92
|
+
and the User-Agent follows them.
|
|
93
|
+
|
|
94
|
+
Within a generation, the tools Zen knows by name — `bash`, `glob`, `grep`,
|
|
95
|
+
`read`, `write`, `edit` — have to be there and described the way OpenCode
|
|
96
|
+
describes them. The first four are added when the host lacks them; the
|
|
97
|
+
other two are only read when the host has them. A tool is never
|
|
98
|
+
discarded: the host's own tool is kept whole, and only the wording sent
|
|
99
|
+
upstream is replaced, so the call the model makes still fits the tool that
|
|
100
|
+
runs it. A tool of any other name is not looked at.
|
|
101
|
+
|
|
102
|
+
A tool that was *added* has nothing behind it: the agent has no such tool
|
|
103
|
+
and a call to it is dropped. Describing it as OpenCode describes it is what
|
|
104
|
+
Zen asks for, and it also makes the model want to call it, so a note saying
|
|
105
|
+
so is placed after the opening Zen reads and before the closing it does
|
|
106
|
+
not. That note is phrased about the one tool on purpose: a blanket "these
|
|
107
|
+
tools are not available" was measured to stop the model using the host's
|
|
108
|
+
own tools as well.
|
|
109
|
+
|
|
110
|
+
The headers sent are exactly those of one generation and no more, and that
|
|
111
|
+
set is a deliberate subset of everything Zen has been seen to accept.
|
|
112
|
+
`x-opencode-request` was measured refused on 2026-10-14 and has never been
|
|
113
|
+
measured to be needed, so it is not sent. `x-session-affinity` and
|
|
114
|
+
`x-session-id` are in OpenCode's own header set upstream and have been
|
|
115
|
+
measured refused, so they are not sent either. A test pins the set by name,
|
|
116
|
+
because adding a header is the easiest way to break this plugin.
|
|
117
|
+
|
|
118
|
+
Now and then Zen closes a streamed Chat Completions reply before its
|
|
119
|
+
`[DONE]`. When the reply has said something and no tool call of it is
|
|
120
|
+
unfinished, it ends there as an ordinary answer, and its text says it was
|
|
121
|
+
cut short; asking again goes on. A reply cut short with a tool call
|
|
122
|
+
unfinished, with nothing said, or collapsed for a client that doesn't
|
|
123
|
+
stream fails with an error naming the reply's id and how much of it came.
|
|
124
|
+
An Anthropic Messages or Responses reply cut short fails too: magpie
|
|
125
|
+
tells it from a whole one and, where the model can, goes on with it.
|
package/core-tools.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Which generation of OpenCode's request Zen's free tier will serve.
|
|
2
|
+
//
|
|
3
|
+
// Zen accepts a request only when the whole of it belongs to one generation:
|
|
4
|
+
// the User-Agent, the header set and the tools have to agree. Two generations
|
|
5
|
+
// are known to be current, and which one applies is the host's to decide:
|
|
6
|
+
//
|
|
7
|
+
// 1.x bash, glob, grep, read - User-Agent opencode/<v> ai-sdk/provider-utils/<v> runtime/bun/<v>
|
|
8
|
+
// 2.x shell, read - User-Agent opencode/<channel>/<version>/<client>
|
|
9
|
+
//
|
|
10
|
+
// Within a generation, the tools named in `read` are compared against Zen's own
|
|
11
|
+
// copy of OpenCode's, and the ones in `required` have to be there. Their data
|
|
12
|
+
// lives in fingerprint.json, which is a capture of a real OpenCode rather than
|
|
13
|
+
// anything written by hand; `bun run capture` takes a new one, and
|
|
14
|
+
// `bun run check:gate` says whether Zen still accepts it.
|
|
15
|
+
//
|
|
16
|
+
// The descriptions there are the opening of OpenCode's, since that is the part
|
|
17
|
+
// Zen reads, with the capture host's scratch directory and OS banner replaced
|
|
18
|
+
// because those belong to the machine the capture ran on.
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
|
|
22
|
+
const captured = JSON.parse(readFileSync(new URL("./fingerprint.json", import.meta.url), "utf8"));
|
|
23
|
+
|
|
24
|
+
export const PROFILES = captured.profiles;
|
|
25
|
+
|
|
26
|
+
/** The names Zen reads the wording of, for a generation. */
|
|
27
|
+
const readNames = (generation) => Object.keys(PROFILES[generation].tools);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The generation a request belongs to, told by the tools it carries. Tool names
|
|
31
|
+
* move far less often than descriptions do, so this survives a rewording that a
|
|
32
|
+
* pinned User-Agent would not.
|
|
33
|
+
*/
|
|
34
|
+
export function generationOf(tools) {
|
|
35
|
+
const names = new Set((tools ?? []).map((tool) => tool?.function?.name ?? tool?.name));
|
|
36
|
+
if (names.has("shell")) return "2.x";
|
|
37
|
+
if (names.has("bash")) return "1.x";
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The User-Agent that generation of OpenCode sends, for a protocol. Each
|
|
43
|
+
* protocol's ai-sdk package bundles its own provider-utils, so 1.x differs by
|
|
44
|
+
* protocol and 2.x does not.
|
|
45
|
+
*/
|
|
46
|
+
export function agentFor(generation, protocol = "chat") {
|
|
47
|
+
const agent = PROFILES[generation].agent;
|
|
48
|
+
return agent.providerUtils
|
|
49
|
+
? `opencode/${agent.opencode} ai-sdk/provider-utils/${agent.providerUtils[protocol]} ${agent.runtime}`
|
|
50
|
+
: `opencode/${agent.channel}/${agent.opencode}/${agent.client}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { readNames };
|
package/fingerprint.json
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_note": "Captured from a real OpenCode by capture.mjs. Refresh with: bun run capture",
|
|
3
|
+
"profiles": {
|
|
4
|
+
"1.x": {
|
|
5
|
+
"agent": {
|
|
6
|
+
"opencode": "1.19.0",
|
|
7
|
+
"providerUtils": {
|
|
8
|
+
"chat": "4.0.23",
|
|
9
|
+
"responses": "4.0.40",
|
|
10
|
+
"anthropic": "4.0.46"
|
|
11
|
+
},
|
|
12
|
+
"runtime": "runtime/bun/1.3.14"
|
|
13
|
+
},
|
|
14
|
+
"required": [
|
|
15
|
+
"bash",
|
|
16
|
+
"glob",
|
|
17
|
+
"grep",
|
|
18
|
+
"read"
|
|
19
|
+
],
|
|
20
|
+
"tools": {
|
|
21
|
+
"bash": {
|
|
22
|
+
"name": "bash",
|
|
23
|
+
"description": "Executes a given Windows PowerShell (5.1) command with optional timeout, ensuring proper handling and security measures.\n\nBe aware: OS: linux, Shell: bash\n\nAll commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.\n\nUse `/tmp` for temporary work outside the workspace. This directory has already been created, already exists, and is pre-approved for external directory access.\n\nIMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.\n\n# Windows PowerShell (5.1) shell notes\n- Use `cmd1; if ($?) { cmd2 }` to chain dependent commands.\n- Use double quotes for interpolated strings (`\"Hello $name\"`), single quotes for verbatim strings.\n- Prefer full cmdlet names like `Get-ChildItem`, `Set-Content`, `Remove-Item`, and `New-Item` over aliases.\n- Use `$(...)` for subexpressions. Use `@(...)` for array expressions.\n- To call a native executable whose path contains spaces, use the call opera",
|
|
24
|
+
"parameters": {
|
|
25
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {
|
|
28
|
+
"command": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "The command to execute"
|
|
31
|
+
},
|
|
32
|
+
"timeout": {
|
|
33
|
+
"minimum": -9007199254740991,
|
|
34
|
+
"exclusiveMinimum": 0,
|
|
35
|
+
"type": "integer",
|
|
36
|
+
"maximum": 9007199254740991,
|
|
37
|
+
"description": "Optional timeout in milliseconds"
|
|
38
|
+
},
|
|
39
|
+
"workdir": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "The working directory to run the command in. Defaults to the current directory. Use this instead of 'cd' commands."
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required": [
|
|
45
|
+
"command"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"glob": {
|
|
50
|
+
"name": "glob",
|
|
51
|
+
"description": "- Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\n- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.\n",
|
|
52
|
+
"parameters": {
|
|
53
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
54
|
+
"type": "object",
|
|
55
|
+
"properties": {
|
|
56
|
+
"pattern": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "The glob pattern to match files against"
|
|
59
|
+
},
|
|
60
|
+
"path": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided."
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"required": [
|
|
66
|
+
"pattern"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"grep": {
|
|
71
|
+
"name": "grep",
|
|
72
|
+
"description": "- Fast content search tool that works with any codebase size\n- Searches file contents using regular expressions\n- Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.)\n- Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\")\n- Returns file paths and line numbers with matching lines\n- Use this tool when you need to find files containing specific patterns\n- If you need to identify/count the number of matches within files, use the Bash tool with `rg` (ripgrep) directly. Do NOT use `grep`.\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\n",
|
|
73
|
+
"parameters": {
|
|
74
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
75
|
+
"type": "object",
|
|
76
|
+
"properties": {
|
|
77
|
+
"pattern": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "The regex pattern to search for in file contents"
|
|
80
|
+
},
|
|
81
|
+
"path": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "The directory to search in. Defaults to the current working directory."
|
|
84
|
+
},
|
|
85
|
+
"include": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "File pattern to include in the search (e.g. \"*.js\", \"*.{ts,tsx}\")"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"required": [
|
|
91
|
+
"pattern"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"read": {
|
|
96
|
+
"name": "read",
|
|
97
|
+
"description": "Read a file or directory from the local filesystem. If the path does not exist, an error is returned.\n\nUsage:\n- The filePath parameter should be an absolute path.\n- By default, this tool returns up to 2000 lines from the start of the file.\n- The offset parameter is the line number to start from (1-indexed).\n- To read later sections, call this tool again with a larger offset.\n- Use the grep tool to find specific content in large files or files with long lines.\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\n- Contents are returned with each line prefixed by its line number as `<line>: <content>`. For example, if a file has contents \"foo\\n\", you will receive \"1: foo\\n\". For directories, entries are returned one per line (without line numbers) with a trailing `/` for subdirectories.\n- Any line longer than 2000 characters is truncated.\n- Call this tool in parallel when you know there are multiple files you want to read.\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\n- This tool can read image files and PDFs and return them as file attachments.\n",
|
|
98
|
+
"parameters": {
|
|
99
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {
|
|
102
|
+
"filePath": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "The absolute path to the file or directory to read"
|
|
105
|
+
},
|
|
106
|
+
"offset": {
|
|
107
|
+
"minimum": 0,
|
|
108
|
+
"type": "integer",
|
|
109
|
+
"maximum": 9007199254740991,
|
|
110
|
+
"description": "The line number to start reading from (1-indexed)"
|
|
111
|
+
},
|
|
112
|
+
"limit": {
|
|
113
|
+
"minimum": 0,
|
|
114
|
+
"type": "integer",
|
|
115
|
+
"maximum": 9007199254740991,
|
|
116
|
+
"description": "The maximum number of lines to read (defaults to 2000)"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"required": [
|
|
120
|
+
"filePath"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"edit": {
|
|
125
|
+
"name": "edit",
|
|
126
|
+
"description": "Performs exact string replacements in files. \n\nUsage:\n- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. \n- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- The edit will FAIL if `oldString` is not found in the file with an error \"oldString not found in content\".\n- The edit will FAIL if `oldString` is found multiple times in the file with an error \"Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match.\" Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every ",
|
|
127
|
+
"parameters": {
|
|
128
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"filePath": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "The absolute path to the file to modify"
|
|
134
|
+
},
|
|
135
|
+
"oldString": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"description": "The text to replace"
|
|
138
|
+
},
|
|
139
|
+
"newString": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"description": "The text to replace it with (must be different from oldString)"
|
|
142
|
+
},
|
|
143
|
+
"replaceAll": {
|
|
144
|
+
"type": "boolean",
|
|
145
|
+
"description": "Replace all occurrences of oldString (default false)"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"required": [
|
|
149
|
+
"filePath",
|
|
150
|
+
"oldString",
|
|
151
|
+
"newString"
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"write": {
|
|
156
|
+
"name": "write",
|
|
157
|
+
"description": "Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\n",
|
|
158
|
+
"parameters": {
|
|
159
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
160
|
+
"type": "object",
|
|
161
|
+
"properties": {
|
|
162
|
+
"content": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"description": "The content to write to the file"
|
|
165
|
+
},
|
|
166
|
+
"filePath": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"description": "The absolute path to the file to write (must be absolute, not relative)"
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"required": [
|
|
172
|
+
"content",
|
|
173
|
+
"filePath"
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"2.x": {
|
|
180
|
+
"agent": {
|
|
181
|
+
"channel": "latest",
|
|
182
|
+
"opencode": "2.0.29",
|
|
183
|
+
"client": "cli"
|
|
184
|
+
},
|
|
185
|
+
"required": [
|
|
186
|
+
"shell",
|
|
187
|
+
"read"
|
|
188
|
+
],
|
|
189
|
+
"tools": {
|
|
190
|
+
"shell": {
|
|
191
|
+
"name": "shell",
|
|
192
|
+
"description": "Execute a shell command and return its output. Commands run on Windows using PowerShell (5.1). Quote file paths containing spaces or special characters. Prefer dedicated tools over shell commands when possible. When output is large, the full result is saved to a file and a truncated preview is returned. Rely on automatic truncation unless filtering the output is more useful. Commands accept an optional timeout, background commands have no timeout by default. Background commands return immediately, and you will be notified when they complete.",
|
|
193
|
+
"parameters": {
|
|
194
|
+
"type": "object",
|
|
195
|
+
"properties": {
|
|
196
|
+
"command": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "Shell command string to execute"
|
|
199
|
+
},
|
|
200
|
+
"workdir": {
|
|
201
|
+
"type": "string",
|
|
202
|
+
"description": "Working directory to execute the command in."
|
|
203
|
+
},
|
|
204
|
+
"timeout": {
|
|
205
|
+
"type": "number",
|
|
206
|
+
"description": "Timeout in milliseconds. Set to 0 to disable the timeout."
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
"required": [
|
|
210
|
+
"command"
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"read": {
|
|
215
|
+
"name": "read",
|
|
216
|
+
"description": "Read the contents of a file or directory. Supports text files, images, and PDFs. Images and PDFs are presented directly to the model. Each text line is prefixed by its 1-based line number as <line>: <content>. The prefix is for reference and is not part of the file content. Directory entries are returned one per line. Use offset and limit to read large files or directories in sections. Prefer one larger read over many small slices, and use grep to find specific content in large files.",
|
|
217
|
+
"parameters": {
|
|
218
|
+
"type": "object",
|
|
219
|
+
"properties": {
|
|
220
|
+
"path": {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"description": "File or directory to read"
|
|
223
|
+
},
|
|
224
|
+
"offset": {
|
|
225
|
+
"type": "number",
|
|
226
|
+
"description": "The line or directory entry to start reading from (1-based)"
|
|
227
|
+
},
|
|
228
|
+
"limit": {
|
|
229
|
+
"type": "number",
|
|
230
|
+
"description": "The maximum number of lines or directory entries to read"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
"required": [
|
|
234
|
+
"path"
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
package/index.mjs
ADDED
package/models.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const PROVIDER = "opencode-zen-free";
|
|
2
|
+
export const SDK = {
|
|
3
|
+
chat: "@ai-sdk/openai-compatible",
|
|
4
|
+
responses: "@ai-sdk/openai",
|
|
5
|
+
anthropic: "@ai-sdk/anthropic",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function parseProtocolDocs(text) {
|
|
9
|
+
const protocols = {};
|
|
10
|
+
const pattern =
|
|
11
|
+
/\|[^|]+\|\s*`?([^|`\s]+)`?\s*\|\s*`[^`]+\/v1\/(chat\/completions|responses|messages|systemone)`/g;
|
|
12
|
+
for (const match of text.matchAll(pattern)) {
|
|
13
|
+
protocols[match[1]] = {
|
|
14
|
+
"chat/completions": "chat",
|
|
15
|
+
responses: "responses",
|
|
16
|
+
messages: "anthropic",
|
|
17
|
+
systemone: "systemone",
|
|
18
|
+
}[match[2]];
|
|
19
|
+
}
|
|
20
|
+
return protocols;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sdkProtocol(npm) {
|
|
24
|
+
if (npm?.includes("anthropic")) return "anthropic";
|
|
25
|
+
if (npm === SDK.responses) return "responses";
|
|
26
|
+
if (npm?.includes("openai-compatible")) return "chat";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isRetired(model) {
|
|
30
|
+
return (
|
|
31
|
+
model.deprecated === true ||
|
|
32
|
+
["deprecated", "retired", "disabled"].includes(model.status ?? model.lifecycle) ||
|
|
33
|
+
model.deprecated_at != null ||
|
|
34
|
+
model.retirement_date != null
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isFree(id, model = {}) {
|
|
39
|
+
return (
|
|
40
|
+
!isRetired(model) && (/free/i.test(id) || (model.cost?.input === 0 && model.cost?.output === 0))
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildModels(ids, provider, protocols, baseURL) {
|
|
45
|
+
const result = {};
|
|
46
|
+
for (const id of ids) {
|
|
47
|
+
if (typeof id !== "string" || !id || id === "__proto__") continue;
|
|
48
|
+
const raw = provider.models?.[id];
|
|
49
|
+
const model = raw ?? {};
|
|
50
|
+
const protocol = protocols[id] ?? (raw && sdkProtocol(model.provider?.npm ?? provider.npm));
|
|
51
|
+
if (!isFree(id, model) || !Object.hasOwn(SDK, protocol)) continue;
|
|
52
|
+
const input = model.modalities?.input ?? ["text"];
|
|
53
|
+
const output = model.modalities?.output ?? ["text"];
|
|
54
|
+
const modalities = (values) =>
|
|
55
|
+
Object.fromEntries(
|
|
56
|
+
["text", "image", "audio", "video", "pdf"].map((name) => [name, values.includes(name)]),
|
|
57
|
+
);
|
|
58
|
+
const efforts = (model.reasoning_options ?? [])
|
|
59
|
+
.filter((option) => option.type === "effort")
|
|
60
|
+
.flatMap((option) => option.values ?? []);
|
|
61
|
+
result[id] = {
|
|
62
|
+
id,
|
|
63
|
+
providerID: PROVIDER,
|
|
64
|
+
name: model.name ?? id,
|
|
65
|
+
api: { id, url: baseURL, npm: SDK[protocol] },
|
|
66
|
+
limit: { context: 32768, output: 4096, ...model.limit },
|
|
67
|
+
capabilities: {
|
|
68
|
+
temperature: model.temperature ?? true,
|
|
69
|
+
reasoning: model.reasoning ?? false,
|
|
70
|
+
attachment: model.attachment ?? input.some((name) => name !== "text"),
|
|
71
|
+
toolcall: model.tool_call ?? true,
|
|
72
|
+
input: modalities(input),
|
|
73
|
+
output: modalities(output),
|
|
74
|
+
interleaved: model.interleaved ?? false,
|
|
75
|
+
},
|
|
76
|
+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
77
|
+
variants: Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }])),
|
|
78
|
+
options: {},
|
|
79
|
+
headers: {},
|
|
80
|
+
status: model.status ?? "active",
|
|
81
|
+
release_date: model.release_date ?? "",
|
|
82
|
+
free: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Offline bootstrap metadata from the OpenCode catalog, 2026-10-06.
|
|
89
|
+
export const bootstrapProvider = {
|
|
90
|
+
npm: SDK.chat,
|
|
91
|
+
models: {
|
|
92
|
+
"big-pickle": {
|
|
93
|
+
name: "Big Pickle",
|
|
94
|
+
cost: { input: 0, output: 0 },
|
|
95
|
+
limit: { context: 200000, input: 160000, output: 32000 },
|
|
96
|
+
reasoning: true,
|
|
97
|
+
tool_call: true,
|
|
98
|
+
interleaved: { field: "reasoning_content" },
|
|
99
|
+
},
|
|
100
|
+
"space-bunny-free": {
|
|
101
|
+
name: "Space Bunny Free",
|
|
102
|
+
cost: { input: 0, output: 0 },
|
|
103
|
+
limit: { context: 1048576, input: 524288, output: 524288 },
|
|
104
|
+
reasoning: true,
|
|
105
|
+
tool_call: true,
|
|
106
|
+
},
|
|
107
|
+
"muse-spark-1.3-contributor-free": {
|
|
108
|
+
name: "Muse Spark 1.3 Free",
|
|
109
|
+
cost: { input: 0, output: 0 },
|
|
110
|
+
provider: { npm: SDK.responses },
|
|
111
|
+
limit: { context: 1048576, output: 131072 },
|
|
112
|
+
reasoning: true,
|
|
113
|
+
tool_call: true,
|
|
114
|
+
modalities: { input: ["text", "image", "video", "pdf", "audio"], output: ["text"] },
|
|
115
|
+
reasoning_options: [
|
|
116
|
+
{ type: "effort", values: ["minimal", "low", "medium", "high", "xhigh"] },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export function configModels(models) {
|
|
123
|
+
return Object.fromEntries(
|
|
124
|
+
Object.entries(models).map(([id, model]) => [
|
|
125
|
+
id,
|
|
126
|
+
{
|
|
127
|
+
id,
|
|
128
|
+
name: model.name,
|
|
129
|
+
provider: { npm: model.api.npm, api: model.api.url },
|
|
130
|
+
limit: model.limit,
|
|
131
|
+
reasoning: model.capabilities.reasoning,
|
|
132
|
+
tool_call: model.capabilities.toolcall,
|
|
133
|
+
attachment: model.capabilities.attachment,
|
|
134
|
+
modalities: {
|
|
135
|
+
input: Object.keys(model.capabilities.input).filter(
|
|
136
|
+
(key) => model.capabilities.input[key],
|
|
137
|
+
),
|
|
138
|
+
output: Object.keys(model.capabilities.output).filter(
|
|
139
|
+
(key) => model.capabilities.output[key],
|
|
140
|
+
),
|
|
141
|
+
},
|
|
142
|
+
cost: { input: 0, output: 0 },
|
|
143
|
+
variants: model.variants,
|
|
144
|
+
free: true,
|
|
145
|
+
},
|
|
146
|
+
]),
|
|
147
|
+
);
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-zen-freex",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "OpenCode Zen's free models, for hosts that are not OpenCode: the public credential, automatic discovery and native streaming protocols",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.mjs",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.mjs",
|
|
9
|
+
"provider.mjs",
|
|
10
|
+
"core-tools.mjs",
|
|
11
|
+
"fingerprint.json",
|
|
12
|
+
"models.mjs",
|
|
13
|
+
"stream.mjs",
|
|
14
|
+
"tool-response.mjs",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"keywords": ["opencode", "opencode-plugin", "zen", "free", "magpie"],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": { "type": "git", "url": "git+https://github.com/tkhs101/opencode-zen-freex.git" },
|
|
21
|
+
"bugs": { "url": "https://github.com/tkhs101/opencode-zen-freex/issues" },
|
|
22
|
+
"homepage": "https://github.com/tkhs101/opencode-zen-freex#readme",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"check:gate": "bun tools-smoke.mjs",
|
|
26
|
+
"capture": "bun capture.mjs"
|
|
27
|
+
},
|
|
28
|
+
"magpie": { "icon": "https://opencode.ai/favicon-v3.ico", "maxConcurrency": 2 }
|
|
6
29
|
}
|