适配opencode工具
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user