适配opencode工具

This commit is contained in:
Sirius
2026-08-12 17:15:40 +08:00
parent 995392e526
commit 5516823fc6
6 changed files with 135 additions and 23 deletions
+67 -10
View File
@@ -95,25 +95,56 @@ curl http://127.0.0.1:18000/v1/chat/completions \
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **command-code-openai-bridge** (461 symbols, 1291 relationships, 29 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **command-code-openai-bridge** (359 symbols, 1057 relationships, 31 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939).
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
## Always Do
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: `detect_changes({scope: "compare", base_ref: "main"})`.
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `query({search_query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`.
- For security review, `explain({target: "fileOrSymbol"})` lists taint findings (source→sink flows; needs `analyze --pdg`).
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
## When Debugging
1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
3. `READ gitnexus://repo/command-code-openai-bridge/process/{processName}` — trace the full execution flow step by step
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
## When Refactoring
- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
## Never Do
- NEVER edit a function, class, or method without first running `impact` on it.
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
- NEVER commit changes without running `detect_changes()` to check affected scope.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
## Tools Quick Reference
| Tool | When to use | Command |
|------|-------------|---------|
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
## Impact Risk Levels
| Depth | Meaning | Action |
|-------|---------|--------|
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
## Resources
@@ -124,6 +155,32 @@ This project is indexed by GitNexus as **command-code-openai-bridge** (461 symbo
| `gitnexus://repo/command-code-openai-bridge/processes` | All execution flows |
| `gitnexus://repo/command-code-openai-bridge/process/{name}` | Step-by-step execution trace |
## Self-Check Before Finishing
Before completing any code modification task, verify:
1. `gitnexus_impact` was run for all modified symbols
2. No HIGH/CRITICAL risk warnings were ignored
3. `gitnexus_detect_changes()` confirms changes match expected scope
4. All d=1 (WILL BREAK) dependents were updated
## Keeping the Index Fresh
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
```bash
npx gitnexus analyze
```
If the index previously included embeddings, preserve them by adding `--embeddings`:
```bash
npx gitnexus analyze --embeddings
```
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
## CLI
| Task | Read this skill file |
+1 -1
View File
@@ -210,7 +210,7 @@ Chat 接口接受标准 function tools、`tool_choice` 和 `parallel_tool_calls`
`stream` 省略或设为 `false` 时返回普通 JSON。普通文本请求的 `stream: true` 会立即建立 SSE 连接并发送 assistant role 块,随后在 Command Code 的 `text_delta` 到达时立即发送对应 SSE chunk,最后发送 `finish_reason: "stop"`、可选 usage 块和 `[DONE]`。工具模式需要先解析和校验完整决策:最终文本会在校验后作为 content chunk 发送;工具调用会作为 `delta.tool_calls` 发送,并以 `finish_reason: "tool_calls"` 结束。Chat structured output 会缓存完整正文,只有 JSON 通过校验或单次修复后才发送 content;连续失败会发送 `invalid_structured_output` 错误和 `[DONE]`,不会先发出无法收回的无效 JSON。`stream_thinking: true` 只作用于非结构化普通文本请求;该兼容格式不是原生 OpenAI reasoning item。传入 `stream_options.include_usage: true` 时,结束前返回 usage 块。
为兼容 Obsidian Copilot、OpenCode 和 LangChain OpenAI-format 客户端,Chat 接口还接受 `temperature``max_tokens``max_completion_tokens``top_p``frequency_penalty``presence_penalty``n: 1``store: false``n: 1` 是 Bridge 实际支持的单候选协议行为;`store: false` 保持每次请求使用 `--no-session` 的无状态行为,不创建服务端 Chat session。Command Code 1.18.0 官方 `--help` 没有 temperature、top-p、frequency/presence penalty 或输出 token 上限的 headless 参数,因此前六个采样/输出限制字段只在协议层接受和校验,不会传给 CLI,也不会用提示词伪造。只要请求显式提供其中任一字段,服务终端会输出警告,HTTP 响应的 `X-Command-Code-Ignored-Parameters` 会准确列出未实际应用的字段;浏览器可通过 CORS exposed header 读取。其他未实现字段仍返回 `400 unsupported_parameter`
为兼容 Obsidian Copilot、OpenCode 和 LangChain OpenAI-format 客户端,Chat 接口还接受 `temperature``max_tokens``max_completion_tokens``top_p``frequency_penalty``presence_penalty``n: 1``store: false``reasoning_effort``n: 1` 是 Bridge 实际支持的单候选协议行为;`store: false` 保持每次请求使用 `--no-session` 的无状态行为,不创建服务端 Chat session。`reasoning_effort` 会覆盖模型配置中的 `effort` 并传给 Command Code 的 `--effort`,与 Responses 的 `reasoning.effort` 行为一致。Command Code 1.18.0 官方 `--help` 没有 temperature、top-p、frequency/presence penalty 或输出 token 上限的 headless 参数,因此前六个采样/输出限制字段只在协议层接受和校验,不会传给 CLI,也不会用提示词伪造。只要请求显式提供其中任一字段,服务终端会输出警告,HTTP 响应的 `X-Command-Code-Ignored-Parameters` 会准确列出未实际应用的字段;浏览器可通过 CORS exposed header 读取。其他未实现字段仍返回 `400 unsupported_parameter`
Chat Completions 和 Responses 共用 Command Code NDJSON 执行层。服务终端会在事件到达时立即显示状态、文本和 Command Code 自身的工具调用。普通文本 Chat 会把每个 `text_delta` 直接写入 SSE;外部 function calling 和 structured output 模式会缓存 `finalText`,避免内部决策或未校验 JSON 进入客户端正文。`response_format` 只约束最终文本:需要调用工具的轮次先按 tools 协议返回,客户端提交工具结果后的最终文本轮次再执行格式校验。Responses 外部 function calling 同样缓存 `finalText`,工具轮次不向客户端输出 assistant 文本。
+15 -1
View File
@@ -123,6 +123,10 @@ const chatResponseFormatSchema = z.discriminatedUnion("type", [
chatJsonSchemaResponseFormatSchema,
]);
export const reasoningEffortSchema = z.enum([
"none", "minimal", "low", "medium", "high", "xhigh", "max",
]);
export const chatCompletionRequestSchema = z.object({
model: z.string().min(1),
messages: z.array(messageSchema).min(1),
@@ -142,6 +146,7 @@ export const chatCompletionRequestSchema = z.object({
parallel_tool_calls: z.boolean().optional(),
response_format: chatResponseFormatSchema.optional().default({ type: "text" }),
store: z.literal(false).optional(),
reasoning_effort: reasoningEffortSchema.optional(),
}).strict().superRefine((request, context) => {
const toolNames = new Set<string>();
for (const [index, tool] of (request.tools ?? []).entries()) {
@@ -214,6 +219,13 @@ export function ignoredChatCompatibilityParameters(request: ChatCompletionReques
return ignoredCompatibilityParameters.filter((key) => request[key] !== undefined);
}
export function chatEffectiveEffort(
request: ChatCompletionRequest,
defaultEffort: string,
): string {
return request.reasoning_effort ?? defaultEffort;
}
export function chatInputAttachments(request: ChatCompletionRequest): PendingAttachment[] {
const attachments: PendingAttachment[] = [];
for (const [messageIndex, message] of request.messages.entries()) {
@@ -354,7 +366,8 @@ function buildToolCommandPrompt(
"只返回以下两个 JSON 对象之一,禁止 Markdown 代码围栏、前后说明和额外字段:",
"最终回答:{\"type\":\"final\",\"content\":\"面向用户的完整回答\"}",
"调用工具:{\"type\":\"tool_calls\",\"calls\":[{\"name\":\"工具名称\",\"arguments\":{}}]}",
"调用工具时,name 必须与 tools 中的名称完全一致,arguments 必须是符合该工具 parameters JSON Schema 的对象。",
"调用工具时,name 必须与 tools 中的名称完全一致,arguments 必须是符合该工具 parameters JSON Schema 的对象;不要把 command、query、path 等参数放在 calls 顶层。",
"错误示例:{\"name\":\"bash\",\"command\":\"echo hi\"}。正确示例:{\"name\":\"bash\",\"arguments\":{\"command\":\"echo hi\"}}。",
"tool_choice 为 none 时必须返回 final;为 required 或指定函数时必须返回 tool_calls。parallel_tool_calls 为 false 时 calls 只能有一项。",
"需要工具提供信息或执行动作时返回 tool_calls 并立即结束;已有足够信息时返回 final。不要在工具调用轮次生成面向用户的正文。",
"response_format 只约束 final.content;返回 tool_calls 时不应用正文格式校验。",
@@ -373,6 +386,7 @@ export function buildToolDecisionRepairPrompt(
return [
"上一次输出不符合外部工具调用传输协议。保持原来的决策意图,只修复 JSON 结构、工具名称或参数。",
"只返回原任务要求的 final 或 tool_calls JSON 对象,禁止 Markdown 代码围栏、前后说明和额外字段。",
"若参数被误放在 calls 顶层,必须移入 arguments 对象,例如把 {\"name\":\"bash\",\"command\":\"...\"} 改为 {\"name\":\"bash\",\"arguments\":{\"command\":\"...\"}}。",
"校验错误:",
errors.join("\n"),
"上一次输出:",
+5 -3
View File
@@ -24,7 +24,7 @@ import {
} from "./command-code.js";
import type { BridgeConfig } from "./config.js";
import { FinalTurnAccumulator } from "./final-turn.js";
import { openAIError, type CommandUsage } from "./openai.js";
import { openAIError, reasoningEffortSchema, type CommandUsage } from "./openai.js";
import type { RequestCoordinator } from "./request-coordinator.js";
import {
parseFunctionToolDecision,
@@ -184,7 +184,7 @@ export const responseRequestSchema = z.object({
previous_response_id: responseIdSchema.nullable().optional().default(null),
metadata: metadataSchema.optional().default({}),
reasoning: z.object({
effort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]),
effort: reasoningEffortSchema,
}).strict().optional(),
text: z.object({
format: responseTextFormatSchema,
@@ -1230,7 +1230,8 @@ function buildResponsesPrompt(
"只返回以下两个 JSON 对象之一,禁止 Markdown 代码围栏、前后说明和额外字段:",
"最终回答:{\"type\":\"final\",\"content\":\"面向用户的完整回答\"}",
"调用工具:{\"type\":\"tool_calls\",\"calls\":[{\"name\":\"工具名称\",\"arguments\":{}}]}",
"调用工具时,name 必须与 tools 中的名称完全一致,arguments 必须是符合该工具 parameters JSON Schema 的对象。",
"调用工具时,name 必须与 tools 中的名称完全一致,arguments 必须是符合该工具 parameters JSON Schema 的对象;不要把 command、query、path 等参数放在 calls 顶层。",
"错误示例:{\"name\":\"bash\",\"command\":\"echo hi\"}。正确示例:{\"name\":\"bash\",\"arguments\":{\"command\":\"echo hi\"}}。",
"tool_choice 为 none 时必须返回 final;为 required 或指定函数时必须返回 tool_calls。parallel_tool_calls 为 false 时 calls 只能有一项。",
"需要工具提供信息或执行动作时返回 tool_calls 并立即结束;已有 function_call_output 且信息足够时返回 final。工具调用轮次禁止生成面向用户的正文。",
formatDirective(request.text.format),
@@ -1415,6 +1416,7 @@ function buildToolDecisionRepairPrompt(
return [
"上一次输出不符合 Responses 外部工具调用传输协议。保持原来的决策意图,只修复 JSON 结构、工具名称或参数。",
"只返回原任务要求的 final 或 tool_calls JSON 对象,禁止 Markdown 代码围栏、前后说明和额外字段。",
"若参数被误放在 calls 顶层,必须移入 arguments 对象,例如把 {\"name\":\"bash\",\"command\":\"...\"} 改为 {\"name\":\"bash\",\"arguments\":{\"command\":\"...\"}}。",
"校验错误:",
errors.join("\n"),
"上一次输出:",
+2 -1
View File
@@ -22,6 +22,7 @@ import {
buildToolStructuredOutputRepairPrompt,
chatResponseTextFormat,
chatCompletionRequestSchema,
chatEffectiveEffort,
chatInputAttachments,
completionResponse,
ignoredChatCompatibilityParameters,
@@ -276,7 +277,7 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
const execution = await executeChatTurn({
config,
cliModel: model.cli_model,
effort: model.effort,
effort: chatEffectiveEffort(parsed, model.effort),
request: parsed,
signal: abortController.signal,
deadline: Date.now() + config.timeout_seconds * 1000,
+45 -7
View File
@@ -139,25 +139,27 @@ function parseCallsDecision(
errors.push(`calls.${index} must be an object`);
continue;
}
const callExtraKeys = Object.keys(rawCall).filter((key) => key !== "name" && key !== "arguments");
if (callExtraKeys.length > 0) {
errors.push(`calls.${index} has unexpected fields: ${callExtraKeys.join(", ")}`);
}
if (typeof rawCall.name !== "string") {
errors.push(`calls.${index}.name must be a string`);
continue;
}
const tool = tools.get(rawCall.name);
const normalizedCall = normalizeToolCallRecord(rawCall, tool);
const callExtraKeys = Object.keys(normalizedCall).filter((key) => key !== "name" && key !== "arguments");
if (callExtraKeys.length > 0) {
errors.push(`calls.${index} has unexpected fields: ${callExtraKeys.join(", ")}`);
}
if (!tool) {
errors.push(`calls.${index}.name references unknown tool '${rawCall.name}'`);
continue;
}
if (typeof toolChoice === "object" && rawCall.name !== toolChoice.name) {
if (typeof toolChoice === "object" && normalizedCall.name !== toolChoice.name) {
errors.push(`calls.${index}.name must be forced tool '${toolChoice.name}'`);
}
const args = normalizeArguments(rawCall.arguments, index, errors);
const args = normalizeArguments(normalizedCall.arguments, index, errors);
if (args === undefined) continue;
const validationErrors = validateFunctionToolArguments(tool, rawCall.name, args);
@@ -167,7 +169,7 @@ function parseCallsDecision(
}
toolCalls.push({
callId: `call_${randomUUID().replaceAll("-", "")}`,
name: rawCall.name,
name: normalizedCall.name as string,
arguments: JSON.stringify(args),
});
}
@@ -249,6 +251,42 @@ function normalizeArguments(
return parsed;
}
function normalizeToolCallRecord(
rawCall: Record<string, unknown>,
tool: FunctionToolDefinition | undefined,
): Record<string, unknown> {
if (!tool) return rawCall;
const propertyNames = toolParameterPropertyNames(tool.parameters);
if (propertyNames.size === 0) return rawCall;
const normalized = { ...rawCall };
const existingArgs = isRecord(normalized.arguments) ? { ...normalized.arguments } : {};
let moved = false;
for (const key of Object.keys(normalized)) {
if (key === "name" || key === "arguments") continue;
if (!propertyNames.has(key)) continue;
if (!(key in existingArgs)) existingArgs[key] = normalized[key];
delete normalized[key];
moved = true;
}
if (moved || normalized.arguments !== undefined) {
normalized.arguments = Object.keys(existingArgs).length > 0
? existingArgs
: normalized.arguments;
}
return normalized;
}
function toolParameterPropertyNames(parameters: Record<string, unknown>): Set<string> {
const properties = parameters.properties;
if (!isRecord(properties)) return new Set();
return new Set(Object.keys(properties));
}
function createValidator(schema: Record<string, unknown>): ValidateFunction {
return new Ajv({ allErrors: true, strict: false }).compile(sanitizeToolParametersSchema(schema));
}