Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/workflows/model-gateway-qualification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Model Gateway Qualification

on:
pull_request:
paths:
- "packages/cli/src/model-gateways/**"
- "packages/cli/src/__tests__/model-gateway-*.test.ts"
- "packages/cli/src/workspace-run.ts"
- "packages/cli/src/framework-registry.ts"
- "packages/cli/src/cli-ui/kit-picker-choices.ts"
- "packages/cli/docs/model-gateways.md"
- "packages/cli/scripts/propose-model-gateway-version-update.ts"
- "packages/cli/scripts/emit-model-gateway-qualification-report.ts"
- ".github/workflows/model-gateway-qualification.yml"
- ".github/workflows/model-gateway-version-discovery.yml"
workflow_dispatch:
inputs:
qualification_mode:
description: "Fast Linux validation or complete three-OS qualification"
required: true
default: full
type: choice
options:
- fast
- full

permissions:
contents: read
pull-requests: read

concurrency:
group: model-gateway-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
qualify:
name: OpenRouter gateways · ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 35
env:
CI: "1"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
strategy:
fail-fast: false
matrix:
# One job per OS qualifies both OpenRouter adapters. A kit axis would
# duplicate the same TypeScript and Python suites without filtering.
os: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.qualification_mode == 'full' && '["ubuntu-latest","macos-latest","windows-latest"]' || '["ubuntu-latest"]') }}

steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "20.19.0"
cache: npm
cache-dependency-path: package-lock.json

- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.10.11"

- name: Install JavaScript dependencies
run: npm ci

- name: Fail if a live OpenRouter key is present
shell: bash
run: |
if [ -n "${OPENROUTER_API_KEY:-}" ]; then
echo "OPENROUTER_API_KEY must not be set during gateway qualification."
exit 1
fi

- name: Run Model Gateway qualification tests
working-directory: packages/cli
shell: bash
run: |
npm exec vitest -- run \
src/__tests__/model-gateway-generated-projects.test.ts \
src/__tests__/model-gateway-workspace-lifecycle.test.ts \
src/__tests__/model-gateway-policy-parity.test.ts \
src/__tests__/model-gateway-project-kits.test.ts \
src/__tests__/model-gateway-registry.test.ts \
src/__tests__/model-gateway-qualification.test.ts \
--reporter=default \
--reporter=json \
--outputFile.json=test-results/model-gateway-qualification/${{ matrix.os }}.vitest.json

- name: Emit machine-readable qualification report
working-directory: packages/cli
run: >-
npm exec tsx -- scripts/emit-model-gateway-qualification-report.ts
--os ${{ matrix.os }}
--vitest-json test-results/model-gateway-qualification/${{ matrix.os }}.vitest.json
--out test-results/model-gateway-qualification/${{ matrix.os }}.json

- name: Upload qualification report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: model-gateway-qualification-${{ matrix.os }}-${{ github.sha }}
path: packages/cli/test-results/model-gateway-qualification/
if-no-files-found: error
retention-days: 30
77 changes: 77 additions & 0 deletions .github/workflows/model-gateway-version-discovery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Model Gateway Version Discovery

on:
schedule:
- cron: "41 7 * * 1"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: model-gateway-version-discovery
cancel-in-progress: true

jobs:
discover:
name: Discover upstream SDK candidates
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
ref: ${{ github.event.repository.default_branch }}

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "20.19.0"
cache: npm
cache-dependency-path: package-lock.json

- name: Install JavaScript dependencies
run: npm ci

- name: Discover upstream version candidates
id: discover
working-directory: packages/cli
shell: bash
run: |
set +e
npm exec tsx -- scripts/propose-model-gateway-version-update.ts \
--baseline src/model-gateways/version-baselines.v1.json \
--report test-results/model-gateway-version-discovery/report.json
status=$?
set -e
echo "status=$status" >> "$GITHUB_OUTPUT"
if [ "$status" -eq 0 ] || [ "$status" -eq 10 ]; then
exit 0
fi
exit "$status"

- name: Upload reviewable candidates
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: model-gateway-version-discovery-${{ github.sha }}
path: packages/cli/test-results/model-gateway-version-discovery/report.json
if-no-files-found: error
retention-days: 30

- name: Publish run summary
if: always()
shell: bash
env:
DISCOVERY_STATUS: ${{ steps.discover.outputs.status }}
run: |
echo "### Model Gateway version discovery" >> "$GITHUB_STEP_SUMMARY"
echo "This workflow does not write the baseline, commit, open a pull request, or admit a Workspai release." >> "$GITHUB_STEP_SUMMARY"
echo "Exit status: ${DISCOVERY_STATUS:-unknown} (0 current, 10 update available)." >> "$GITHUB_STEP_SUMMARY"
REPORT=packages/cli/test-results/model-gateway-version-discovery/report.json
if [ -f "$REPORT" ]; then
echo "" >> "$GITHUB_STEP_SUMMARY"
echo '```json' >> "$GITHUB_STEP_SUMMARY"
python3 -c "import json,pathlib; d=json.loads(pathlib.Path('$REPORT').read_text()); print(json.dumps({'outcome': d.get('outcome'), 'summary': d.get('summary')}, indent=2))" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
fi
24 changes: 24 additions & 0 deletions contracts/create-planner-capabilities.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,30 @@
"versionPolicy": "tested-baseline",
"moduleSupport": false,
"workspacePythonEngine": "none"
},
{
"id": "gateway.openrouter.typescript",
"runtime": "node",
"framework": "openrouter",
"plannerFramework": "openrouter",
"category": "gateway",
"owner": "workspai",
"stability": "stable",
"versionPolicy": "tested-baseline",
"moduleSupport": false,
"workspacePythonEngine": "none"
},
{
"id": "gateway.openrouter.python",
"runtime": "python",
"framework": "openrouter",
"plannerFramework": "openrouter",
"category": "gateway",
"owner": "workspai",
"stability": "stable",
"versionPolicy": "tested-baseline",
"moduleSupport": false,
"workspacePythonEngine": "none"
}
],
"officialCreate": [
Expand Down
4 changes: 3 additions & 1 deletion contracts/runtime-command-surface.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -4373,7 +4373,9 @@
"agent.microsoft.python",
"agent.microsoft.dotnet",
"agent.openai.python",
"agent.openai.typescript"
"agent.openai.typescript",
"gateway.openrouter.typescript",
"gateway.openrouter.python"
],
"officialCreate": [
"frontend.nextjs",
Expand Down
4 changes: 3 additions & 1 deletion contracts/workspace-intelligence-architecture.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,9 @@
"agent.microsoft.python",
"agent.microsoft.dotnet",
"agent.openai.python",
"agent.openai.typescript"
"agent.openai.typescript",
"gateway.openrouter.typescript",
"gateway.openrouter.python"
],
"officialCreate": [
"frontend.nextjs",
Expand Down
4 changes: 4 additions & 0 deletions contracts/workspace-intelligence/workspace-model.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"service",
"worker",
"agent",
"gateway",
"platform",
"library",
"infra",
Expand All @@ -66,6 +67,7 @@
"desktop",
"extension",
"agent",
"gateway",
"platform",
"library",
"infrastructure",
Expand Down Expand Up @@ -97,6 +99,7 @@
"extension",
"worker",
"agent",
"gateway",
"platform",
"library",
"infra",
Expand All @@ -112,6 +115,7 @@
"desktop",
"extension",
"agent",
"gateway",
"platform",
"library",
"infrastructure",
Expand Down
47 changes: 47 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Introduced the first-class **AI Gateway** Create category (`Unified model
access and routing`) with OpenRouter Client SDK kits
`gateway.openrouter.typescript` (`@openrouter/sdk` `1.3.11`) and
`gateway.openrouter.python` (`openrouter` `1.2.11`). Generated projects are
server-owned, pin reviewed SDK baselines, keep the model as runtime
configuration, and do not call a model during Create. The official Go SDK
remains beta and is not included on the Create surface. Attach is unsupported
for this surface. Version discovery and three-OS qualification workflows are
read-only and do not admit a Workspai release by themselves.

### Changed

- Workspace Model kind/category enums, Create planner native kits, interactive
Create, Workspace Run/Verify polyglot lifecycle, and doctor runtime adapters
now recognize `gateway` as a first-class project category distinct from
Agent Framework.
- Model Gateway qualification CI runs one job per OS that qualifies both
TypeScript and Python adapters, instead of an unused kit axis that duplicated
the same suites. Job-level cache env no longer uses `runner.temp` or a
duplicate `NPM_CONFIG_CACHE`; `setup-node` owns the npm cache.

### Fixed

- Generated Python `max_price` validation now rejects overflow-to-infinity
strings, NaN, and non-finite values while preserving accepted numeric strings
for the SDK. Stream cleanup in both generated gateways is once-only and
prefers iterator `return()`/`close()` over a second SDK close/cancel.
- Workspace Run `start` executes a single materialized argv step without a
shell, so Python gateway projects whose install path contains spaces fail
closed on missing credentials instead of being split by `/bin/sh`.
- `validateCommand` accepts a space-containing executable path only when that
path exists, then falls back to the first token so commands such as
`go build ./...` still resolve through `which`.
- Model Gateway version discovery is a read-only `discover → propose → review
→ qualify → merge` path. Registry and GitHub must agree before a baseline
proposal can be written. Workspace lifecycle qualification no longer returns
green when package installation fails.
- Qualification treats sparse workspace-run init receipts (`errorCategory:
unknown` and `Stage failed with exit code 1`, including missing npm/pip
excerpts) as registry infrastructure, not product failure, and retries once.
- Doctor and the workspace model keep an OpenRouter gateway as framework
`openrouter` and kind `gateway`, instead of collapsing it to generic Python
or Node.js. Python src-layout packages are no longer blocked for a missing
`src/__init__.py` when a nested package already has one.

## [0.76.0] - 2026-09-20

### Added
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ npx workspai create
```

Choose whether to create a workspace, scaffold a project, or add existing
software. Project starters are grouped as Backend, Frontend, Desktop, and
Extension.
software. Project starters are grouped as Backend, Frontend, Desktop, AI
Agent, AI Gateway, Extension, and Gaming.

```bash
# Optional global installation
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/contracts/create-planner-capabilities.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,30 @@
"versionPolicy": "tested-baseline",
"moduleSupport": false,
"workspacePythonEngine": "none"
},
{
"id": "gateway.openrouter.typescript",
"runtime": "node",
"framework": "openrouter",
"plannerFramework": "openrouter",
"category": "gateway",
"owner": "workspai",
"stability": "stable",
"versionPolicy": "tested-baseline",
"moduleSupport": false,
"workspacePythonEngine": "none"
},
{
"id": "gateway.openrouter.python",
"runtime": "python",
"framework": "openrouter",
"plannerFramework": "openrouter",
"category": "gateway",
"owner": "workspai",
"stability": "stable",
"versionPolicy": "tested-baseline",
"moduleSupport": false,
"workspacePythonEngine": "none"
}
],
"officialCreate": [
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/contracts/runtime-command-surface.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -4373,7 +4373,9 @@
"agent.microsoft.python",
"agent.microsoft.dotnet",
"agent.openai.python",
"agent.openai.typescript"
"agent.openai.typescript",
"gateway.openrouter.typescript",
"gateway.openrouter.python"
],
"officialCreate": [
"frontend.nextjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,9 @@
"agent.microsoft.python",
"agent.microsoft.dotnet",
"agent.openai.python",
"agent.openai.typescript"
"agent.openai.typescript",
"gateway.openrouter.typescript",
"gateway.openrouter.python"
],
"officialCreate": [
"frontend.nextjs",
Expand Down
Loading
Loading