feat: add extract-feature and export-spec portability skills
Two new OpenSpec skills for porting features to sandboxed codebases: - /opsx:extract-feature generates minimal, printable code recipes - /opsx:export-spec generates compact specs for AI-assisted reimplementation Both support cumulative dependency analysis across archived changes. Includes first export of migrate-to-semantic-kernel in all three formats: code recipe (~120 lines), portable spec (~40 lines), OpenSpec variant (~25 lines). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
131
.claude/commands/opsx/export-spec.md
Normal file
131
.claude/commands/opsx/export-spec.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: "OPSX: Export Spec"
|
||||
description: Export a feature as a compact, portable spec for AI-assisted reimplementation on a sandboxed machine
|
||||
category: Workflow
|
||||
tags: [workflow, portability, experimental]
|
||||
---
|
||||
|
||||
Export a feature as a portable spec for AI-assisted reimplementation.
|
||||
|
||||
Instead of retyping code, you retype a compact spec. The AI on the sandbox generates the code.
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:export-spec` is a change name (active or archived), or a description of the feature. If omitted, prompt for selection.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Identify the source feature**
|
||||
|
||||
Same selection logic as `/opsx:extract-feature`:
|
||||
- Check active changes and archive for the change name
|
||||
- If not found, prompt with **AskUserQuestion tool**
|
||||
- Read all artifacts: `proposal.md`, `design.md`, `tasks.md`, specs
|
||||
|
||||
2. **Analyze dependency chain (cumulative mode)**
|
||||
|
||||
Features often build on each other. Before generating the spec, determine
|
||||
what the target feature depends on.
|
||||
|
||||
a. Read all archived change proposals in `openspec/changes/archive/` (sorted by date).
|
||||
b. Build a dependency graph: what does the target require? What's superseded?
|
||||
c. Ask the user:
|
||||
> "This feature depends on earlier changes. How should I scope the spec?"
|
||||
> 1. **Cumulative** — include all foundation (recommended for fresh codebase)
|
||||
> 2. **Delta only** — just this change (target already has foundation)
|
||||
> 3. **Custom** — pick which dependencies to include
|
||||
|
||||
d. In cumulative mode: merge into one coherent spec, skip superseded components.
|
||||
e. In delta mode: add an "Assumes" section listing what must already exist.
|
||||
|
||||
3. **Read the actual implementation**
|
||||
|
||||
Read all source files created or modified by this feature (and dependencies if cumulative).
|
||||
The spec must reflect what was actually built, not just what was planned.
|
||||
|
||||
4. **Determine the target context**
|
||||
|
||||
Use **AskUserQuestion tool** to ask:
|
||||
> "Tell me about the target codebase:
|
||||
> 1. Project name / root namespace
|
||||
> 2. Existing stack (ASP.NET Core? Blazor? MudBlazor?)
|
||||
> 3. Does it already have any of these? (controllers, DI setup, chat endpoint)
|
||||
> 4. Does the target have OpenSpec? GitHub Copilot? Claude Code?"
|
||||
|
||||
This shapes what the spec assumes vs what it must specify.
|
||||
|
||||
5. **Generate the portable spec**
|
||||
|
||||
Create a single markdown document that is:
|
||||
- **Compact**: Target ~30-50 lines for a medium feature
|
||||
- **Precise**: Unambiguous enough for an AI to implement correctly
|
||||
- **Self-contained**: No references to external files or repos
|
||||
- **Stack-aware**: Uses the right terminology for the target stack
|
||||
|
||||
Structure:
|
||||
|
||||
```markdown
|
||||
# Feature: <Name>
|
||||
## Target: <project name> (<stack>)
|
||||
|
||||
## Packages
|
||||
<list with versions>
|
||||
|
||||
## Architecture
|
||||
<2-3 sentence overview>
|
||||
|
||||
## Components
|
||||
### <Component>: <path hint>
|
||||
- <What it does>
|
||||
- <Key behavior>
|
||||
- <Interface/contract>
|
||||
|
||||
## Contracts
|
||||
<API shapes, model definitions — things that MUST be exact>
|
||||
|
||||
## Wiring
|
||||
<DI registration, middleware order, config keys — dependency order>
|
||||
|
||||
## Behavior
|
||||
<Non-obvious requirements>
|
||||
```
|
||||
|
||||
**Compression strategies:**
|
||||
- Use bullet points, not prose
|
||||
- Specify contracts precisely (field names, types, API shapes)
|
||||
- Let the AI infer standard patterns
|
||||
- Only specify non-obvious behavior
|
||||
- Omit anything the AI would do by default
|
||||
|
||||
6. **Estimate typing effort**
|
||||
|
||||
Count characters in the spec. Compare to the code recipe equivalent.
|
||||
Show the compression ratio.
|
||||
|
||||
7. **Optionally generate an OpenSpec-compatible version**
|
||||
|
||||
If the target has OpenSpec, also generate:
|
||||
- A `proposal.md` (minimal — 5-10 lines)
|
||||
- A `tasks.md` (implementation steps)
|
||||
|
||||
Save as: `openspec/exports/<change-name>-openspec.md`
|
||||
|
||||
8. **Write the output**
|
||||
|
||||
Save to: `openspec/exports/<change-name>-spec.md`
|
||||
Display the full content for review.
|
||||
|
||||
**Guardrails**
|
||||
- Prioritize precision over brevity — ambiguity wastes more time than length
|
||||
- Always include exact field names, types, and API shapes
|
||||
- Include non-obvious gotchas (like /v1 base URL requirements)
|
||||
- Mental test: could an AI implement this correctly without seeing the original code?
|
||||
- If too complex for ~50 lines, split into multiple specs by component
|
||||
- Always show the compression ratio
|
||||
- Must be readable when printed in monospace — no wide tables or long lines
|
||||
- In cumulative mode, the spec must read as one coherent feature, not a list of changes
|
||||
- Skip superseded components — always describe the latest version
|
||||
- In delta mode, add an "Assumes" section so the target AI knows what must exist
|
||||
- In the output header, note which changes were included and which were skipped
|
||||
|
||||
ARGUMENTS: based on the above
|
||||
113
.claude/commands/opsx/extract-feature.md
Normal file
113
.claude/commands/opsx/extract-feature.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
name: "OPSX: Extract Feature"
|
||||
description: Extract a feature into a minimal, printable code recipe for manual reimplementation
|
||||
category: Workflow
|
||||
tags: [workflow, portability, experimental]
|
||||
---
|
||||
|
||||
Extract a feature into a minimal, printable code recipe for manual reimplementation.
|
||||
|
||||
Generates a markdown document with:
|
||||
- Package dependencies
|
||||
- Ordered code blocks (no comments, no boilerplate)
|
||||
- Clear markers for generic vs domain-specific code
|
||||
|
||||
Print it, take it to the sandbox, type it in.
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:extract-feature` is a change name (active or archived), a git commit range, or a list of files. If omitted, prompt for selection.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Identify the source feature**
|
||||
|
||||
If a change name is provided:
|
||||
- Check active changes: `openspec list --json`
|
||||
- Check archive: look in `openspec/changes/archive/` for directories ending with the name
|
||||
- If found, read its artifacts: `proposal.md`, `design.md`, `tasks.md`
|
||||
|
||||
If no name provided:
|
||||
- Run `openspec list --json` and list archived changes
|
||||
- Use **AskUserQuestion tool** to let the user select
|
||||
|
||||
If a file list or commit range is provided instead:
|
||||
- Read those files directly
|
||||
- Identify the feature from the code
|
||||
|
||||
2. **Analyze dependency chain (cumulative mode)**
|
||||
|
||||
Features often build on each other. Before generating the recipe, determine
|
||||
what the target feature depends on.
|
||||
|
||||
a. Read all archived change proposals in `openspec/changes/archive/` (sorted by date).
|
||||
b. Build a dependency graph: what does the target require? What's superseded?
|
||||
c. Ask the user:
|
||||
> "This feature depends on earlier changes. How should I scope the recipe?"
|
||||
> 1. **Cumulative** — include all foundation code (recommended for fresh codebase)
|
||||
> 2. **Delta only** — just this change's code (target already has foundation)
|
||||
> 3. **Custom** — pick which dependencies to include
|
||||
|
||||
d. In cumulative mode: merge the chain, skip superseded code, use final file versions.
|
||||
e. In delta mode: include only the selected change's code.
|
||||
|
||||
3. **Analyze the feature scope**
|
||||
|
||||
From the change artifacts and/or code (across full dependency chain if cumulative), determine:
|
||||
- Which files were created or modified
|
||||
- What NuGet/npm packages were added
|
||||
- What the dependency order is (e.g., models before controllers)
|
||||
- What is generic infrastructure vs domain-specific logic
|
||||
|
||||
Read all relevant source files. Always read the final current version of each file.
|
||||
|
||||
4. **Generate the code recipe**
|
||||
|
||||
Create a markdown document with these sections, in this order:
|
||||
|
||||
**Header**: Feature name, source change, date
|
||||
|
||||
**Prerequisites**: Package references with exact versions
|
||||
|
||||
**Steps**: Ordered by dependency. Each step contains:
|
||||
- Step number and action (e.g., "New file", "Add to existing file", "Modify")
|
||||
- Target path (relative, adaptable)
|
||||
- Namespace placeholder: `__YOUR_NAMESPACE__` where the target project namespace goes
|
||||
- **Code block**: The actual code, stripped of:
|
||||
- All comments (// and /* */ and /// XML docs)
|
||||
- Redundant blank lines
|
||||
- Verbose variable names that can be shortened
|
||||
- Any code not directly related to the feature
|
||||
- If the step modifies an existing file: show only the code to add, with a brief marker for insertion point (e.g., "Add after AddControllers()")
|
||||
|
||||
**Domain-Specific Sections**: Clearly marked with `// ADAPT:` prefix explaining what to change for the target domain
|
||||
|
||||
5. **Optimize for retyping**
|
||||
|
||||
Review the generated document and:
|
||||
- Merge small files if they can be combined
|
||||
- Remove any using statements that the IDE will auto-add
|
||||
- Shorten any unnecessarily verbose code
|
||||
- Ensure no step exceeds ~40 lines (split if needed)
|
||||
- Add line counts per step so the user can estimate effort
|
||||
- Total the overall line count at the top
|
||||
|
||||
6. **Write the output**
|
||||
|
||||
Save to: `openspec/exports/<change-name>-recipe.md`
|
||||
|
||||
Also display the full content so the user can review it immediately.
|
||||
|
||||
**Guardrails**
|
||||
- Never include comments in code blocks — the goal is minimum keystrokes
|
||||
- Always read the actual current source files, not just the change artifacts
|
||||
- Preserve compilation order: models -> services -> controllers -> DI registration
|
||||
- Mark domain-specific code clearly so the user knows what to adapt vs copy verbatim
|
||||
- Keep each step self-contained — the user may take breaks between steps
|
||||
- If a feature spans more than ~200 lines of stripped code, warn the user and suggest using `/opsx:export-spec` instead
|
||||
- Output must be valid markdown that renders well when printed
|
||||
- When in cumulative mode, skip superseded code — always use the latest version of each file
|
||||
- In the output header, show which changes were included and which were skipped
|
||||
- If a dependency chain is long (4+ changes), suggest `/opsx:export-spec` as more efficient
|
||||
|
||||
ARGUMENTS: based on the above
|
||||
217
.claude/skills/openspec-export-spec/SKILL.md
Normal file
217
.claude/skills/openspec-export-spec/SKILL.md
Normal file
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: openspec-export-spec
|
||||
description: Export a feature as a compact, portable spec that an AI agent (Copilot, Claude) on a sandboxed machine can implement from. Optimized for minimal hand-typing.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Export a feature as a portable spec for AI-assisted reimplementation on a sandboxed machine.
|
||||
|
||||
Instead of retyping code, you retype a compact spec. The AI on the sandbox (Copilot, Claude, OpenSpec) generates the code from the spec.
|
||||
|
||||
---
|
||||
|
||||
**Input**: A change name (active or archived), or a description of the feature. If omitted, prompt for selection.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Identify the source feature**
|
||||
|
||||
Same as `openspec-extract-feature` step 1:
|
||||
- Check active changes and archive for the change name
|
||||
- If not found, prompt with **AskUserQuestion tool**
|
||||
- Read all artifacts: `proposal.md`, `design.md`, `tasks.md`, specs
|
||||
|
||||
2. **Analyze dependency chain (cumulative mode)**
|
||||
|
||||
Features often build on each other. Before generating the spec, determine
|
||||
what the target feature depends on.
|
||||
|
||||
a. **Read all archived change proposals** in `openspec/changes/archive/` (sorted by date).
|
||||
For each, read `proposal.md` to understand what it adds and what it depends on.
|
||||
|
||||
b. **Build a dependency graph**:
|
||||
- Which changes does the target feature require?
|
||||
- Which earlier changes are superseded? (e.g., if Change 6 replaces Change 3's
|
||||
implementation, include Change 6's version, not Change 3's)
|
||||
- Which changes are unrelated and can be skipped?
|
||||
|
||||
c. **Ask the user** using **AskUserQuestion tool**:
|
||||
> "This feature depends on earlier changes. How should I scope the spec?"
|
||||
>
|
||||
> Options:
|
||||
> 1. **Cumulative** — include all foundation this feature needs (recommended if target is a fresh codebase)
|
||||
> 2. **Delta only** — just what this specific change adds (use if target already has the foundation)
|
||||
> 3. **Custom** — let me pick which dependencies to include
|
||||
|
||||
Show the dependency chain so the user can decide.
|
||||
|
||||
d. **In cumulative mode**: The spec covers the entire stack from foundation to feature.
|
||||
- Merge packages, components, wiring from all included changes
|
||||
- Skip superseded components (use the latest version)
|
||||
- The spec should read as a single coherent feature, not a list of changes
|
||||
|
||||
e. **In delta mode**: The spec covers only the selected change.
|
||||
- Add a "Assumes" section listing what must already exist in the target
|
||||
|
||||
3. **Read the actual implementation**
|
||||
|
||||
Read all source files that were created or modified by this feature
|
||||
(and its dependencies if cumulative).
|
||||
The spec must reflect what was actually built, not just what was planned.
|
||||
|
||||
4. **Determine the target context**
|
||||
|
||||
Use **AskUserQuestion tool** to ask:
|
||||
> "Tell me about the target codebase:
|
||||
> 1. Project name / root namespace
|
||||
> 2. Existing stack (ASP.NET Core? Blazor? MudBlazor?)
|
||||
> 3. Does it already have any of these? (controllers, DI setup, chat endpoint)
|
||||
> 4. Does the target have OpenSpec? GitHub Copilot? Claude Code?"
|
||||
|
||||
This shapes what the spec assumes vs what it must specify.
|
||||
|
||||
5. **Generate the portable spec**
|
||||
|
||||
Create a single markdown document that is:
|
||||
- **Compact**: Target ~30-50 lines for a medium feature
|
||||
- **Precise**: Unambiguous enough for an AI to implement correctly
|
||||
- **Self-contained**: No references to external files or repos
|
||||
- **Stack-aware**: Uses the right terminology for the target stack
|
||||
|
||||
Structure:
|
||||
|
||||
```markdown
|
||||
# Feature: <Name>
|
||||
## Target: <project name> (<stack>)
|
||||
|
||||
## Packages
|
||||
<list with versions>
|
||||
|
||||
## Architecture
|
||||
<2-3 sentence overview of how the pieces fit together>
|
||||
|
||||
## Components
|
||||
|
||||
### <Component 1>: <path hint>
|
||||
- <What it does — 1 line>
|
||||
- <Key behavior — 1 line>
|
||||
- <Interface/contract — 1 line>
|
||||
|
||||
### <Component 2>: <path hint>
|
||||
...
|
||||
|
||||
## Contracts
|
||||
<API shapes, model definitions, SSE formats — the things that MUST be exact>
|
||||
|
||||
## Wiring
|
||||
<DI registration, middleware order, configuration keys — in dependency order>
|
||||
|
||||
## Behavior
|
||||
<Key behavioral requirements that aren't obvious from the structure>
|
||||
```
|
||||
|
||||
**Compression strategies:**
|
||||
- Use bullet points, not prose
|
||||
- Specify contracts precisely (field names, types, API shapes)
|
||||
- Let the AI infer standard patterns (error handling, null checks, etc.)
|
||||
- Only specify non-obvious behavior (the surprising parts)
|
||||
- Omit anything the AI would do by default for the given stack
|
||||
|
||||
6. **Estimate typing effort**
|
||||
|
||||
Count characters in the spec. Compare to the code recipe equivalent.
|
||||
Show the compression ratio:
|
||||
```
|
||||
Code recipe: ~120 lines to type
|
||||
This spec: ~35 lines to type
|
||||
Compression: 3.4x
|
||||
```
|
||||
|
||||
7. **Optionally generate an OpenSpec-compatible version**
|
||||
|
||||
If the target has OpenSpec, also generate a version structured as:
|
||||
- A `proposal.md` (minimal — 5-10 lines)
|
||||
- A `tasks.md` (implementation steps the target AI follows)
|
||||
|
||||
These can be even more compact because OpenSpec provides the scaffolding.
|
||||
|
||||
Save this variant alongside the main spec.
|
||||
|
||||
8. **Write the output**
|
||||
|
||||
Save to: `openspec/exports/<change-name>-spec.md`
|
||||
If OpenSpec variant: `openspec/exports/<change-name>-openspec.md`
|
||||
|
||||
Display the full content for review.
|
||||
|
||||
**Output Format**
|
||||
|
||||
```markdown
|
||||
# Feature: Semantic Kernel Chat with Tool Calling
|
||||
## Target: ApplicationX (ASP.NET Core + Blazor WASM + MudBlazor)
|
||||
|
||||
## Packages
|
||||
- Microsoft.SemanticKernel 1.74.0
|
||||
- Microsoft.SemanticKernel.Connectors.OpenAI 1.74.0
|
||||
|
||||
## Architecture
|
||||
POST /api/chat endpoint accepts messages, runs them through SK's chat completion
|
||||
with auto tool calling enabled, streams response as SSE. An ExtractionPlugin
|
||||
validates structured data extracted by the LLM.
|
||||
|
||||
## Components
|
||||
|
||||
### ChatController: Controllers/ChatController.cs
|
||||
- POST endpoint, injects Kernel via DI
|
||||
- Converts ChatMessage[] to SK ChatHistory
|
||||
- Streams via GetStreamingChatMessageContentsAsync
|
||||
- Outputs SSE: `data: {"text":"..."}\n\n` then `data: [DONE]\n\n`
|
||||
|
||||
### ExtractionPlugin: Plugins/ExtractionPlugin.cs
|
||||
- [KernelFunction("validate_extracted_fields")]
|
||||
- Accepts JSON string, deserializes to ExtractedFields
|
||||
- Returns {"isValid": bool, "errors": string[]}
|
||||
|
||||
### ExtractedFields: Models/ExtractedFields.cs
|
||||
- Required: Client(string), Project(string), Hours(decimal), Rate(decimal), Currency(string), Date(string)
|
||||
- Optional: Description(string), PoNumber(string)
|
||||
|
||||
### ValidationResult: Models/ValidationResult.cs
|
||||
- IsValid(bool), Errors(List<string>)
|
||||
|
||||
### ChatRequest/ChatMessage: Shared/Models/
|
||||
- ChatRequest: Messages(List<ChatMessage>)
|
||||
- ChatMessage: Role(string), Content(string)
|
||||
|
||||
## Wiring (Program.cs, add after AddControllers)
|
||||
- AddOpenAIChatCompletion(model, endpoint with /v1 suffix, apiKey)
|
||||
- AddKernel()
|
||||
- AddSingleton<ExtractionPlugin>()
|
||||
- UseCors for Blazor client origin
|
||||
|
||||
## Behavior
|
||||
- FunctionChoiceBehavior.Auto() enables autonomous tool calling
|
||||
- SK's built-in limit prevents runaway tool call loops
|
||||
- Plugin imported per-request via _kernel.ImportPluginFromObject
|
||||
- Base URL must include /v1 — OpenAI SDK appends chat/completions directly
|
||||
```
|
||||
|
||||
**Lines to type: ~35 | Code equivalent: ~150 lines | Compression: 4.3x**
|
||||
|
||||
**Guardrails**
|
||||
- Prioritize precision over brevity — an ambiguous spec wastes more time than a slightly longer one
|
||||
- Always include exact field names, types, and API shapes — these are the hardest to guess
|
||||
- Include non-obvious gotchas (like the /v1 base URL requirement)
|
||||
- Test the spec mentally: could an AI implement this correctly without seeing the original code?
|
||||
- If the feature is too complex for a single spec page (~50+ lines), split into multiple specs by component
|
||||
- Always show the compression ratio so the user can decide between spec and code recipe
|
||||
- The spec must be readable when printed in monospace — no wide tables or long lines
|
||||
- In cumulative mode, the spec must read as one coherent feature — not a list of sequential changes
|
||||
- Skip superseded components — always describe the latest version of each piece
|
||||
- In delta mode, add an "Assumes" section so the target AI knows what must already exist
|
||||
- In the output header, note which changes were included and which were skipped
|
||||
170
.claude/skills/openspec-extract-feature/SKILL.md
Normal file
170
.claude/skills/openspec-extract-feature/SKILL.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: openspec-extract-feature
|
||||
description: Extract a feature from a change (archived or active) into a minimal, printable code recipe optimized for manual retyping into a sandboxed codebase.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Extract a feature into a minimal, printable code recipe for manual reimplementation.
|
||||
|
||||
Generates a markdown document with:
|
||||
- Package dependencies
|
||||
- Ordered code blocks (no comments, no boilerplate)
|
||||
- Clear markers for generic vs domain-specific code
|
||||
|
||||
Print it, take it to the sandbox, type it in.
|
||||
|
||||
---
|
||||
|
||||
**Input**: A change name (active or archived), a git commit range, or a list of files. If omitted, prompt for selection.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Identify the source feature**
|
||||
|
||||
If a change name is provided:
|
||||
- Check active changes: `openspec list --json`
|
||||
- Check archive: look in `openspec/changes/archive/` for directories ending with the name
|
||||
- If found, read its artifacts: `proposal.md`, `design.md`, `tasks.md`
|
||||
|
||||
If no name provided:
|
||||
- Run `openspec list --json` and list archived changes
|
||||
- Use **AskUserQuestion tool** to let the user select
|
||||
|
||||
If a file list or commit range is provided instead:
|
||||
- Read those files directly
|
||||
- Identify the feature from the code
|
||||
|
||||
2. **Analyze dependency chain (cumulative mode)**
|
||||
|
||||
Features are often not independent — they build on each other. Before generating
|
||||
the recipe, determine what the target feature depends on.
|
||||
|
||||
a. **Read all archived change proposals** in `openspec/changes/archive/` (sorted by date).
|
||||
For each, read `proposal.md` to understand what it adds and what it depends on.
|
||||
|
||||
b. **Build a dependency graph**:
|
||||
- Which changes does the target feature require?
|
||||
- Which earlier changes are superseded? (e.g., if Change 6 replaces Change 3's
|
||||
implementation, include Change 6's version, not Change 3's)
|
||||
- Which changes are unrelated and can be skipped?
|
||||
|
||||
c. **Ask the user** using **AskUserQuestion tool**:
|
||||
> "This feature depends on earlier changes. How should I scope the recipe?"
|
||||
>
|
||||
> Options:
|
||||
> 1. **Cumulative** — include all foundation code this feature needs (recommended if target is a fresh codebase)
|
||||
> 2. **Delta only** — just the code this specific change adds (use if target already has the foundation)
|
||||
> 3. **Custom** — let me pick which dependencies to include
|
||||
|
||||
Show the dependency chain so the user can decide, e.g.:
|
||||
```
|
||||
migrate-to-semantic-kernel depends on:
|
||||
← basic-chat-interface (UI + shared models)
|
||||
← wire-responses-api (superseded — SK replaces the proxy)
|
||||
← multi-turn-conversations (chat history)
|
||||
```
|
||||
|
||||
d. **In cumulative mode**: Merge the dependency chain into a single coherent recipe.
|
||||
- Walk changes in dependency order
|
||||
- Skip superseded code (use the latest version of each file)
|
||||
- Collapse packages from all changes into one prerequisites section
|
||||
- If a file was created in Change 2 and modified in Change 5, include only the final version
|
||||
|
||||
e. **In delta mode**: Include only the code from the selected change.
|
||||
|
||||
3. **Analyze the feature scope**
|
||||
|
||||
From the change artifacts and/or code, determine:
|
||||
- Which files were created or modified (across the full dependency chain if cumulative)
|
||||
- What NuGet/npm packages were added
|
||||
- What the dependency order is (e.g., models before controllers)
|
||||
- What is generic infrastructure vs domain-specific logic
|
||||
|
||||
Read all relevant source files in the current codebase to get the actual current code.
|
||||
**Always read the final current version of each file**, not intermediate versions.
|
||||
|
||||
3. **Generate the code recipe**
|
||||
|
||||
Create a markdown document with these sections, in this order:
|
||||
|
||||
**Header**: Feature name, source change, date
|
||||
|
||||
**Prerequisites**: Package references with exact versions
|
||||
|
||||
**Steps**: Ordered by dependency. Each step contains:
|
||||
- Step number and action (e.g., "New file", "Add to existing file", "Modify")
|
||||
- Target path (relative, adaptable)
|
||||
- Namespace placeholder: `__YOUR_NAMESPACE__` where the target project namespace goes
|
||||
- **Code block**: The actual code, stripped of:
|
||||
- All comments (// and /* */ and /// XML docs)
|
||||
- Redundant blank lines
|
||||
- Verbose variable names that can be shortened
|
||||
- Any code not directly related to the feature
|
||||
- If the step modifies an existing file: show only the code to add, with a brief marker for insertion point (e.g., "Add after AddControllers()")
|
||||
|
||||
**Domain-Specific Sections**: Clearly marked with `// ADAPT:` prefix explaining what to change for the target domain
|
||||
|
||||
4. **Optimize for retyping**
|
||||
|
||||
Review the generated document and:
|
||||
- Merge small files if they can be combined
|
||||
- Remove any using statements that the IDE will auto-add
|
||||
- Shorten any unnecessarily verbose code
|
||||
- Ensure no step exceeds ~40 lines (split if needed)
|
||||
- Add line counts per step so the user can estimate effort
|
||||
- Total the overall line count at the top
|
||||
|
||||
5. **Write the output**
|
||||
|
||||
Save to: `openspec/exports/<change-name>-recipe.md`
|
||||
|
||||
Also display the full content so the user can review it immediately.
|
||||
|
||||
**Output Format**
|
||||
|
||||
```markdown
|
||||
# Feature Recipe: <Feature Name>
|
||||
**Source**: <change-name> | **Lines to type**: ~<N>
|
||||
|
||||
## Prerequisites
|
||||
- `Microsoft.SemanticKernel` 1.74.0
|
||||
- `Microsoft.SemanticKernel.Connectors.OpenAI` 1.74.0
|
||||
|
||||
## Step 1: <Action> — <path> (~N lines)
|
||||
|
||||
```csharp
|
||||
<stripped code>
|
||||
```
|
||||
|
||||
## Step 2: <Action> — <path> (~N lines)
|
||||
|
||||
Insert after `<marker>`:
|
||||
```csharp
|
||||
<stripped code>
|
||||
```
|
||||
|
||||
## Domain-Specific (adapt these)
|
||||
|
||||
### ExtractionPlugin.cs
|
||||
```csharp
|
||||
// ADAPT: Replace field names and validation logic for your domain
|
||||
<code>
|
||||
```
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Never include comments in code blocks — the goal is minimum keystrokes
|
||||
- Always read the actual current source files, not just the change artifacts
|
||||
- Preserve compilation order: models → services → controllers → DI registration
|
||||
- Mark domain-specific code clearly so the user knows what to adapt vs copy verbatim
|
||||
- Keep each step self-contained — the user may take breaks between steps
|
||||
- If a feature spans more than ~200 lines of stripped code, warn the user and suggest using `/opsx:export-spec` instead
|
||||
- Output must be valid markdown that renders well when printed
|
||||
- When in cumulative mode, skip superseded code — always use the latest version of each file
|
||||
- In the output header, show which changes were included and which were skipped (with reason)
|
||||
- If a dependency chain is long (4+ changes), suggest `/opsx:export-spec` as a more efficient alternative
|
||||
76
openspec/exports/migrate-to-semantic-kernel-openspec.md
Normal file
76
openspec/exports/migrate-to-semantic-kernel-openspec.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# OpenSpec Portable Variant
|
||||
## For hand-typing into target's OpenSpec on the sandbox
|
||||
|
||||
Type these two files into the target's `openspec/changes/add-sk-chat/` directory.
|
||||
Then run `/opsx:apply` and let the AI implement from the tasks.
|
||||
|
||||
**Lines to type**: ~25 | **vs code recipe**: ~120 lines | **Compression**: 4.8x
|
||||
|
||||
---
|
||||
|
||||
## File 1: proposal.md
|
||||
|
||||
```markdown
|
||||
# Add SK Chat Endpoint with Tool Calling
|
||||
|
||||
## Why
|
||||
Need an AI chat endpoint that streams responses and supports
|
||||
autonomous tool calling for structured data extraction/validation.
|
||||
|
||||
## What Changes
|
||||
- Add Semantic Kernel 1.74.0 with OpenAI connector
|
||||
- POST /api/chat endpoint streaming SSE via SK
|
||||
- ExtractionPlugin with [KernelFunction] for field validation
|
||||
- Shared models: ChatRequest, ChatMessage, ExtractedFields, ValidationResult
|
||||
|
||||
## Impact
|
||||
- API project: new controller, plugin, DI wiring
|
||||
- Shared project: new models
|
||||
- Config: ResponsesApi section in appsettings.json
|
||||
```
|
||||
|
||||
## File 2: tasks.md
|
||||
|
||||
```markdown
|
||||
## 1. Shared Models
|
||||
- [ ] 1.1 Create ChatMessage (Role string, Content string, Timestamp DateTime)
|
||||
- [ ] 1.2 Create ChatRequest (Messages List<ChatMessage>)
|
||||
- [ ] 1.3 Create ExtractedFields with required fields: Client, Project, Hours(decimal?), Rate(decimal?), Currency, Date; optional: Description, PoNumber
|
||||
- [ ] 1.4 Create ValidationResult (IsValid bool, Errors List<string>)
|
||||
|
||||
## 2. Extraction Plugin
|
||||
- [ ] 2.1 Create ExtractionPlugin class with [KernelFunction("validate_extracted_fields")]
|
||||
- [ ] 2.2 Accepts fieldsJson string, deserializes to ExtractedFields with PropertyNameCaseInsensitive
|
||||
- [ ] 2.3 Validates required fields non-null/non-empty, decimals > 0
|
||||
- [ ] 2.4 Returns JSON serialized ValidationResult
|
||||
|
||||
## 3. Chat Controller
|
||||
- [ ] 3.1 Create ChatController [ApiController] Route("api/[controller]") injecting Kernel
|
||||
- [ ] 3.2 POST endpoint: set response to text/event-stream, no-cache
|
||||
- [ ] 3.3 Convert request messages to SK ChatHistory (user/assistant roles)
|
||||
- [ ] 3.4 Import ExtractionPlugin per-request via _kernel.ImportPluginFromObject
|
||||
- [ ] 3.5 Use OpenAIPromptExecutionSettings with FunctionChoiceBehavior.Auto()
|
||||
- [ ] 3.6 Stream via GetStreamingChatMessageContentsAsync, emit SSE: data: {"text":"..."}\n\n
|
||||
- [ ] 3.7 Emit data: [DONE]\n\n on completion
|
||||
- [ ] 3.8 Handle HttpRequestException (emit error SSE), TaskCanceledException (silent)
|
||||
|
||||
## 4. DI Wiring (Program.cs)
|
||||
- [ ] 4.1 Add using Microsoft.SemanticKernel at top of Program.cs
|
||||
- [ ] 4.2 Read BaseUrl and Model from config "ResponsesApi" section
|
||||
- [ ] 4.3 AddOpenAIChatCompletion(modelId, endpoint with /v1 suffix, apiKey)
|
||||
- [ ] 4.4 AddKernel()
|
||||
- [ ] 4.5 AddSingleton<ExtractionPlugin>()
|
||||
|
||||
## 5. Configuration
|
||||
- [ ] 5.1 Add ResponsesApi section to appsettings.json: BaseUrl "http://localhost:8317/v1", Model "claude-sonnet-4-6"
|
||||
- [ ] 5.2 Add NuGet: Microsoft.SemanticKernel 1.74.0, Microsoft.SemanticKernel.Connectors.OpenAI 1.74.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage on sandbox
|
||||
|
||||
1. Create the change: `openspec new change "add-sk-chat"`
|
||||
2. Type `proposal.md` into `openspec/changes/add-sk-chat/proposal.md`
|
||||
3. Type `tasks.md` into `openspec/changes/add-sk-chat/tasks.md`
|
||||
4. Run `/opsx:apply add-sk-chat` — the AI implements all tasks
|
||||
251
openspec/exports/migrate-to-semantic-kernel-recipe.md
Normal file
251
openspec/exports/migrate-to-semantic-kernel-recipe.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Feature Recipe: Semantic Kernel Chat with Tool Calling
|
||||
|
||||
**Source**: migrate-to-semantic-kernel + wire-responses-api | **Lines to type**: ~120
|
||||
**Included**: wire-responses-api (SSE streaming), migrate-to-semantic-kernel (SK + plugins)
|
||||
**Skipped**: wire-responses-api's manual HttpClient proxy (superseded by SK)
|
||||
**Skipped**: basic-chat-interface (UI — not selected), multi-turn-conversations (not selected)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Add to your API `.csproj`:
|
||||
```xml
|
||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.74.0" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.74.0" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: New file — Shared/Models/ChatMessage.cs (~6 lines)
|
||||
|
||||
```csharp
|
||||
namespace __YOUR_NAMESPACE__.Shared.Models
|
||||
{
|
||||
public class ChatMessage
|
||||
{
|
||||
public string Role { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: New file — Shared/Models/ChatRequest.cs (~6 lines)
|
||||
|
||||
```csharp
|
||||
namespace __YOUR_NAMESPACE__.Shared.Models
|
||||
{
|
||||
public class ChatRequest
|
||||
{
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: New file — Shared/Models/ExtractedFields.cs (~12 lines)
|
||||
|
||||
```csharp
|
||||
// ADAPT: Replace these fields with your domain's extraction schema
|
||||
namespace __YOUR_NAMESPACE__.Shared.Models
|
||||
{
|
||||
public class ExtractedFields
|
||||
{
|
||||
public string? Client { get; set; }
|
||||
public string? Project { get; set; }
|
||||
public decimal? Hours { get; set; }
|
||||
public decimal? Rate { get; set; }
|
||||
public string? Currency { get; set; }
|
||||
public string? Date { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? PoNumber { get; set; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: New file — Shared/Models/ValidationResult.cs (~5 lines)
|
||||
|
||||
```csharp
|
||||
namespace __YOUR_NAMESPACE__.Shared.Models
|
||||
{
|
||||
public class ValidationResult
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public List<string> Errors { get; set; } = new();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: New file — Plugins/ExtractionPlugin.cs (~35 lines)
|
||||
|
||||
```csharp
|
||||
// ADAPT: Change RequiredFields and validation logic to match your ExtractedFields
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using __YOUR_NAMESPACE__.Shared.Models;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace __YOUR_NAMESPACE__.Api.Plugins
|
||||
{
|
||||
public class ExtractionPlugin
|
||||
{
|
||||
private static readonly string[] RequiredFields =
|
||||
{ "Client", "Project", "Hours", "Rate", "Currency", "Date" };
|
||||
|
||||
[KernelFunction("validate_extracted_fields")]
|
||||
[Description("Validates extracted key-value fields against the required schema. " +
|
||||
"Call this after extracting fields from natural language text to check " +
|
||||
"that all required fields (Client, Project, Hours, Rate, Currency, Date) " +
|
||||
"are present and correctly typed. Returns validation result with any errors.")]
|
||||
public string ValidateExtractedFields(
|
||||
[Description("JSON string of extracted fields")] string fieldsJson)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
ExtractedFields? fields;
|
||||
try
|
||||
{
|
||||
fields = JsonSerializer.Deserialize<ExtractedFields>(fieldsJson,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.Errors.Add($"Invalid JSON: {ex.Message}");
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
|
||||
if (fields == null)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.Errors.Add("Deserialized fields object is null");
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fields.Client))
|
||||
result.Errors.Add("Missing required field: Client");
|
||||
if (string.IsNullOrWhiteSpace(fields.Project))
|
||||
result.Errors.Add("Missing required field: Project");
|
||||
if (fields.Hours == null || fields.Hours <= 0)
|
||||
result.Errors.Add("Missing or invalid required field: Hours (must be positive)");
|
||||
if (fields.Rate == null || fields.Rate <= 0)
|
||||
result.Errors.Add("Missing or invalid required field: Rate (must be positive)");
|
||||
if (string.IsNullOrWhiteSpace(fields.Currency))
|
||||
result.Errors.Add("Missing required field: Currency");
|
||||
if (string.IsNullOrWhiteSpace(fields.Date))
|
||||
result.Errors.Add("Missing required field: Date");
|
||||
|
||||
result.IsValid = result.Errors.Count == 0;
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 6: New file — Controllers/ChatController.cs (~40 lines)
|
||||
|
||||
```csharp
|
||||
using System.Text.Json;
|
||||
using __YOUR_NAMESPACE__.Api.Plugins;
|
||||
using __YOUR_NAMESPACE__.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace __YOUR_NAMESPACE__.Api.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ChatController : ControllerBase
|
||||
{
|
||||
private readonly Kernel _kernel;
|
||||
|
||||
public ChatController(Kernel kernel) { _kernel = kernel; }
|
||||
|
||||
[HttpPost]
|
||||
public async Task Post([FromBody] ChatRequest request)
|
||||
{
|
||||
Response.ContentType = "text/event-stream";
|
||||
Response.Headers["Cache-Control"] = "no-cache";
|
||||
try
|
||||
{
|
||||
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
|
||||
var chatHistory = new ChatHistory();
|
||||
foreach (var msg in request.Messages)
|
||||
{
|
||||
if (msg.Role == "user") chatHistory.AddUserMessage(msg.Content);
|
||||
else if (msg.Role == "assistant") chatHistory.AddAssistantMessage(msg.Content);
|
||||
}
|
||||
|
||||
var plugin = HttpContext.RequestServices.GetRequiredService<ExtractionPlugin>();
|
||||
_kernel.ImportPluginFromObject(plugin, "Extraction");
|
||||
|
||||
var settings = new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
|
||||
await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(
|
||||
chatHistory, settings, _kernel, HttpContext.RequestAborted))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(chunk.Content))
|
||||
{
|
||||
await WriteSSEAsync($"{{\"text\":{JsonSerializer.Serialize(chunk.Content)}}}");
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
await WriteSSEAsync("[DONE]");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
await WriteSSEAsync($"{{\"error\":{JsonSerializer.Serialize($"Failed to reach LLM service: {ex.Message}")}}}");
|
||||
await WriteSSEAsync("[DONE]");
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
}
|
||||
|
||||
private async Task WriteSSEAsync(string data)
|
||||
{
|
||||
await Response.WriteAsync($"data: {data}\n\n");
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 7: Add to Program.cs (~12 lines)
|
||||
|
||||
Add `using Microsoft.SemanticKernel;` at the top.
|
||||
|
||||
Insert after `builder.Services.AddControllers();`:
|
||||
```csharp
|
||||
// ADAPT: Change BaseUrl and Model for your proxy/LLM
|
||||
var baseUrl = builder.Configuration["ResponsesApi:BaseUrl"] ?? "http://localhost:8317/v1";
|
||||
var model = builder.Configuration["ResponsesApi:Model"] ?? "claude-sonnet-4-6";
|
||||
|
||||
builder.Services.AddOpenAIChatCompletion(
|
||||
modelId: model,
|
||||
endpoint: new Uri(baseUrl),
|
||||
apiKey: builder.Configuration["ResponsesApi:ApiKey"] ?? "not-needed");
|
||||
builder.Services.AddKernel();
|
||||
builder.Services.AddSingleton<__YOUR_NAMESPACE__.Api.Plugins.ExtractionPlugin>();
|
||||
```
|
||||
|
||||
## Step 8: Add to appsettings.json (~4 lines)
|
||||
|
||||
Add this section:
|
||||
```json
|
||||
"ResponsesApi": {
|
||||
"BaseUrl": "http://localhost:8317/v1",
|
||||
"Model": "claude-sonnet-4-6"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Base URL must include `/v1`** — the OpenAI SDK appends `chat/completions` directly, so without `/v1` you get a 404
|
||||
- **`using Microsoft.SemanticKernel;`** is required in Program.cs for the `AddOpenAIChatCompletion` extension method to resolve
|
||||
- **Plugin is imported per-request** via `_kernel.ImportPluginFromObject` — do not register it in the kernel at startup
|
||||
- **CORS**: If your Blazor client is on a different port, add CORS policy in Program.cs before `app.Run()`
|
||||
63
openspec/exports/migrate-to-semantic-kernel-spec.md
Normal file
63
openspec/exports/migrate-to-semantic-kernel-spec.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Feature: SK Chat Endpoint with Tool Calling
|
||||
## Target: ApplicationX (ASP.NET Core + Blazor WASM + MudBlazor)
|
||||
|
||||
**Included**: wire-responses-api (superseded — SK version used), migrate-to-semantic-kernel
|
||||
**Lines to type**: ~40 | **Code equivalent**: ~120 lines | **Compression**: 3x
|
||||
|
||||
## Packages
|
||||
- Microsoft.SemanticKernel 1.74.0
|
||||
- Microsoft.SemanticKernel.Connectors.OpenAI 1.74.0
|
||||
|
||||
## Architecture
|
||||
POST /api/chat accepts conversation messages, runs them through Semantic
|
||||
Kernel's chat completion with auto tool calling, streams response as SSE.
|
||||
An ExtractionPlugin lets the LLM validate structured data it extracts
|
||||
from natural language, retrying autonomously before escalating to the user.
|
||||
|
||||
## Components
|
||||
|
||||
### ChatController: Controllers/ChatController.cs
|
||||
- [ApiController] POST endpoint, injects Kernel via DI
|
||||
- Converts List<ChatMessage> to SK ChatHistory (user/assistant roles)
|
||||
- Imports ExtractionPlugin per-request via _kernel.ImportPluginFromObject
|
||||
- Uses OpenAIPromptExecutionSettings with FunctionChoiceBehavior.Auto()
|
||||
- Streams via GetStreamingChatMessageContentsAsync, skips empty chunks
|
||||
- SSE output: `data: {"text":"..."}\n\n` per chunk, `data: [DONE]\n\n` at end
|
||||
- Error: `data: {"error":"..."}\n\n` then [DONE]
|
||||
- Catches TaskCanceledException silently (client disconnect)
|
||||
|
||||
### ExtractionPlugin: Plugins/ExtractionPlugin.cs
|
||||
- [KernelFunction("validate_extracted_fields")]
|
||||
- [Description] tells LLM: validates extracted fields against required schema
|
||||
- Accepts string fieldsJson, deserializes to ExtractedFields
|
||||
- Checks required fields non-null/non-empty, decimals > 0
|
||||
- Returns JSON: {"IsValid": bool, "Errors": ["..."]}
|
||||
|
||||
### ExtractedFields: Shared/Models/ExtractedFields.cs
|
||||
- Required: Client(string?), Project(string?), Hours(decimal?), Rate(decimal?), Currency(string?), Date(string?)
|
||||
- Optional: Description(string?), PoNumber(string?)
|
||||
|
||||
### ValidationResult: Shared/Models/ValidationResult.cs
|
||||
- IsValid(bool), Errors(List<string>)
|
||||
|
||||
### ChatRequest + ChatMessage: Shared/Models/
|
||||
- ChatRequest: Messages(List<ChatMessage>)
|
||||
- ChatMessage: Role(string), Content(string), Timestamp(DateTime)
|
||||
|
||||
## Wiring (Program.cs, after AddControllers)
|
||||
1. `using Microsoft.SemanticKernel;` at top (required for extension methods)
|
||||
2. Read BaseUrl and Model from config section "ResponsesApi"
|
||||
3. AddOpenAIChatCompletion(modelId, endpoint: new Uri(baseUrl), apiKey)
|
||||
4. AddKernel()
|
||||
5. AddSingleton<ExtractionPlugin>()
|
||||
6. CORS policy if Blazor client on different port
|
||||
|
||||
## Config (appsettings.json)
|
||||
- ResponsesApi:BaseUrl = "http://localhost:8317/v1"
|
||||
- ResponsesApi:Model = "claude-sonnet-4-6"
|
||||
|
||||
## Gotchas
|
||||
- Base URL MUST include /v1 — OpenAI SDK appends chat/completions directly
|
||||
- Plugin imported per-request, not at startup (avoids kernel state leaks)
|
||||
- SK has built-in auto-invoke limit — no need to set max retries
|
||||
- JsonSerializerOptions needs PropertyNameCaseInsensitive = true for deserialization
|
||||
Reference in New Issue
Block a user