# internmore **Repository Path**: opencode-plugin/internmore ## Basic Information - **Project Name**: internmore - **Description**: internmore-支持多key轮询 - **Primary Language**: TypeScript - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-28 - **Last Updated**: 2026-10-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Intern More OpenCode Provider Plugin **提供商 ID**: `internmore` | **显示名称**: Intern More | **Base URL**: `https://discovery-api.intern-ai.org.cn/v1` OpenCode 插件,集成 Intern AI(discovery-api.intern-ai.org.cn)网关,支持 API Key 多密钥轮询以缓解速率限制。 ## 功能 - **多 Key 轮询** — 输入多个 API Key(逗号分隔),自动循环切换,减缓 `429` 速率限制 - **错误容错** — 单个 Key 连续错误 3 次自动跳过,全部不可用时重置计数 - **模型支持** — 内置 7 个模型(覆盖 256K / 1M 上下文,含视觉与推理模型) - **思考内容分离** — 声明 `interleaved` 后 OpenCode 渲染独立思考区;插件注入 `thinking` 开关兜底 - **模型别名映射** — 上游部分模型 id 大小写敏感(`Agents-A1`、`Atria-Dawn-Preview`),OpenCode 上送全小写别名,插件改写后请求 - **短暂错误重试** — 上游 5xx 与偶发 `422 upstream_error` 统一转为 429,交由 OpenCode 重试 - **未授权兜底** — 未配置 Key 时直接返回 401 并提示授权,不触发无意义重试 - **首 chunk 超时保护** — SSE 流 15 秒无首块时返回 429,交由 OpenCode 重试(防上游挂起) - **调试日志** — `~/.internmore/debug.log`,由环境变量控制开关 - **凭证存储** — API Key 持久化到 `~/.internmore/auth.json` ## 安装 ### 前提 - Node.js 22+ - OpenCode ### 步骤 ```bash # 1. 进入项目 cd internmore # 2. 安装依赖 npm install # 3. 构建 npm run build ``` ## 配置 ### 1. 配置 opencode.json ```json // opencode.json { "plugins": [ "/path/to/internmore" ] } ``` ### 2. 添加凭据 在 OpenCode 中执行 `/connect` 命令,选择 **Intern More**: ``` ┌ 注册赠送10墨点(约5亿tokens) │ │ 创建 API Key:https://discovery.intern-ai.org.cn/token-plan/home?tabIndex=0 │ │ 模型 id 列表:https://discovery.intern-ai.org.cn/token-plan/home?tabIndex=1 │ │ 输入 API Keys(多key用逗号分隔) │ ──────────────────────────────── │ sk-key1,sk-key2,sk-key3 └ enter ``` > 多个 Key 用逗号分隔,插件会自动轮询使用。未授权时模型请求会返回 > `未授权:请通过 /connect 命令点击 Intern More 进行授权`。 ### 3. 选择模型 执行 `/models` 命令,选择可用模型: | Model ID | 上游模型 | 描述 | 上下文 | 最大输出 | 输入 | |----------|----------|------|--------|----------|------| | `agents-a1` | `Agents-A1` | Intern AI Agents-A1 | 160K | 32K | text+image | | `atria-dawn-preview` | `Atria-Dawn-Preview` | Atria Dawn Preview | 200K | 32K | text | | `deepseek-v4-flash` | `deepseek-v4-flash-0731` | DeepSeek V4 Flash 0731 | 48K | 32K | text | | `deepseek-v4-flash-vision` | `deepseek-v4-flash-vision` | DeepSeek V4 Flash Vision | 124K | 32K | text+image | | `intern-s2` | `intern-s2` | Intern S2 | 48K | 32K | text+image | | `minimax-m3` | `minimax-m3` | MiniMax M3 | 16K | 8K | text | | `qwen3.8-27b` | `qwen3.8-27b` | Qwen 3.8 27B | 30K | 8K | text+image | > **模型别名映射**:OpenCode 上送的是表中 `Model ID` 列(全小写别名),插件通过它匹配 > `models.jsonc` 中的模型定义,再用「上游模型」列的真实 id 请求上游。 > 上游部分模型 id **大小写敏感**(`Agents-A1`、`Atria-Dawn-Preview`),插件自动改写。 > **已移除模型**(2026-10-10,实测后裁剪):`deepseek-v4-pro`、`glm-5.3`、`kimi-k2.6` > > 原因:上游 discovery 网关按模型实施**极小的单请求 token 预算**(预检查 `prompt_tokens + max_tokens` > 总量:glm-5.3 / deepseek-v4-pro 约 4~6K、kimi-k2.6 仅约 1~4K),而 OpenCode 每个请求固定实发 > `max_tokens = min(limit.output, 32000)`,任何真实编码会话必然触发 `429 quota_exceeded`, > 且该限制与 key 无关、多 key 轮换无效。上下文极小,编码任务体验很差,不推荐。 > 机制分析与完整实测数据见 [docs/upstream-quotas.md](docs/upstream-quotas.md)。 > > `minimax-m3` 保留,limit 已调至实测安全档(上下文 16K / 输出 8K),仅适合轻量任务。 > > **表中「上下文 / 最大输出」为 2026-10-10 实测安全档**(单请求 `prompt + max_tokens` 总量上限的保守取值), > 非上游声明值——声明值普遍虚高:`deepseek-v4-flash` 声称 1M 上下文、实测配额约 53K; > `minimax-m3` 声称 1M、实测约 16K;`Agents-A1` 声称 256K、实测通过 168K。 > OpenCode 实发 `max_tokens = min(limit.output, 32000)`。完整实测数据见 [docs/upstream-quotas.md](docs/upstream-quotas.md)。 ## 思考内容处理 推理模型(`deepseek-v4-*`、`minimax-m3`、`qwen3.8-27b`、`atria-dawn-preview`) 声明 `interleaved: { field: 'reasoning' }`,OpenCode 渲染为独立思考区。 插件额外在请求体注入 `thinking: { type: "enabled" }` 作为开关兜底 (防止上游偶发把推理混入 `content`)。`reasoning_effort` 由 OpenCode 按用户选择的 档位自动传入(SDK 在 `transformRequestBody` 之前写入),插件仅透传、不写死。 ## 多 Key 轮询机制 ``` 请求 → chat.headers → KeyManager.getNextKey() ↓ ┌──────────────┐ │ key1 ← 首次 │ │ key2 ← 下次 │ │ key3 ← 再下次 │ └──────────────┘ ↓ 写入 Authorization → 发起 API 请求 ``` - 循环轮询:`key1 → key2 → key3 → key1 → ...` - 错误追踪:每个 Key 连续失败 3 次后跳过 - 自动恢复:所有 Key 都被跳过时重置计数 - 成功自愈:请求成功后清零该 Key 的错误计数 ## 错误处理 自定义 `fetch` 统一将上游错误转换为 OpenCode 可解析的标准 JSON 错误流: | 场景 | 行为 | |------|------| | 401 / 403 | 原样返回上游 Response;`isRetryable=false` 立即报错(不重试),保留上游原始错误消息 | | 429 | 原样返回上游 Response + `Retry-After`;由 OpenCode 判断重试 | | 5xx | 返回固定 429 错误流,触发 OpenCode 重试 | | 422 `upstream_error` | 返回固定 429 错误流,触发 OpenCode 重试(上游推理偶发失败) | | 400 校验错误 | 保留原状态码立即报错(参数校验错误,重试无意义) | | 408 / 409 | 返回原状态码,OpenCode 视为可重试 | | 网络异常 | 返回 502 标准错误流,不向 UI 抛异常 | | 未授权(无 Key) | 返回 401 并提示 `/connect` 授权,不发真实请求 | | 首 chunk 15s 超时 | 返回 429 触发 OpenCode 重试(防上游挂起) | > 本插件不自行节流/重试,429 与短暂性错误均交由 OpenCode 重试机制处理。 ## 调试日志 通过环境变量开启调试日志: ```bash # 方式一:全局 DEBUG DEBUG=1 opencode # 方式二:仅 Intern More OPENCODE_INTERNMORE_DEBUG=1 opencode ``` 日志位置:`%USERPROFILE%\.internmore\debug.log` > 环境变量在**插件加载时**读取,需在启动 OpenCode 前设置。 ### 日志格式 可读 TEXT 单行格式(对齐 agnesmore/sensemore 约定),按 append 追加: ``` [2026-09-28T08:25:05.573Z] [INFO ] Auth loader initialized {"keyCount":8,"keys":["1:sk_EXAM...a1b2","2:sk_EXAM...c3d4"]} [2026-09-28T08:25:05.595Z] [INFO ] Request start {"seq":1,"key":"sk_EXAM...a1b2","keyPos":"1/8","model":"glm-5.3","endpoint":"/v1/chat/completions"} [2026-09-28T08:25:09.864Z] [INFO ] Request success {"seq":1,"key":"sk_EXAM...a1b2","keyPos":"1/8","status":200,"stream":false,"ms":4046} ``` ### 确认 Key 轮询情况 **每个请求(无论成功或失败)都会记录所用 key**,用于确认轮询是否生效: | 字段 | 含义 | |------|------| | `seq` | 请求序号(插件生命周期内自增) | | `key` | 该请求实际使用的 API Key(掩码,保留前 8 位 + 末 4 位) | | `keyPos` | 该 key 在池中的位置,如 `3/8` | | `model` | 请求的模型(已改写为上游真实 id) | | `endpoint` | 请求路径,如 `/v1/chat/completions` | | `status` / `stream` / `ms` | 响应状态码 / 是否流式 / 耗时毫秒 | 观察 `seq` 与 `keyPos` 的对应关系即可确认轮询: ``` seq:1 keyPos:1/8 seq:2 keyPos:2/8 seq:3 keyPos:3/8 ... seq:9 keyPos:1/8 ← 8 把 key 用完一轮后回到第 1 把 ``` ## 项目结构 ``` internmore/ ├── package.json ├── tsconfig.json ├── jsonc/ # 配置文件副本(provider.jsonc 与 models.jsonc) ├── src/ │ ├── index.ts # 主入口:auth hook + config hook + chat.headers + 共享 fetch │ ├── constants.ts # 提供商配置(外置 JSONC 读取)、模型定义加载 │ ├── plugin/ │ │ ├── auth.ts # 凭证读写 ~/.internmore/auth.json │ │ └── key-manager.ts # 多 Key 轮询算法 │ └── utils/ │ └── debug-logger.ts # 文件日志(无 console.error) └── dist/ # 构建产物 ``` ## Key 黑名单(blacklist.json) KeyManager 会为每个 Key 维护错误计数,持久性错误(`401/403/400` 等)触发 `reportError`(计数 +1), 当 `errorCount >= 3` 时该 Key 被临时拉黑,轮询时自动跳过;请求成功一次即触发 `reportSuccess` 复位计数。 黑名单状态通过快照文件跨重启保留,重启后自动恢复各 Key 的错误计数。 快照文件格式(与插件写盘格式一致): ```json { "updatedAt": 1759800000000, "keys": [ { "key": "sk-xxx1", "errorCount": 3, "blocked": true }, { "key": "sk-xxx2", "errorCount": 0, "blocked": false } ] } ``` 目标路径:`%USERPROFILE%\.internmore\blacklist.json` 复制方法(可选):将项目 `jsonc/blacklist.jsonc` 复制到 `%USERPROFILE%\.internmore\` 并重命名为 `blacklist.json`。不复制也不影响运行——插件仅在内存中计数,不跨重启保留;快照读写失败仅记录 debug 警告,绝不中断主流程。 ## 配置文件 插件运行所需的配置文件位于本仓库 ./jsonc/ 目录下(provider.jsonc 与 models.jsonc)。使用前将这两个文件复制到用户目录 %USERPROFILE%\.internmore/ 下(缺失时插件启动会 fail fast 报错)。复制后可按需编辑模型列表与提供商信息(支持注释),修改后重启插件生效。 - `provider.jsonc` — 提供商信息:`{ "id": "internmore", "name": "Intern More", "type": "key" }` - `models.jsonc` — 模型定义:key 为 OpenCode 上送的模型别名(全小写),`id` 为上游真实模型 id,含 `limit`(context/output)、`reasoning`、`interleaved`、`capabilities.vision`、`modalities`、`cost` 字段 - `auth.json` — 授权后自动写入的多 apiKey 列表(`/connect` 时生成) ## 开发 ```bash npm run typecheck # 类型检查 npm run build # 构建到 dist/ ```