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
8 changes: 5 additions & 3 deletions .github/workflows/live-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ jobs:
COMETAPI_KEY: ${{ secrets.COMETAPI_KEY }}
COMETAPI_LIVE_SMOKE: "1"
COMETAPI_SMOKE_MODEL: ${{ vars.COMETAPI_SMOKE_MODEL || 'gpt-5.6-sol' }}
COMETAPI_LIVE_REQUEST_LIMIT: "3"
COMETAPI_LIVE_RETRY_BUDGET: "1"
COMETAPI_LIVE_REQUEST_LIMIT: "4"
COMETAPI_LIVE_MAX_OUTPUT_TOKENS: "16"
COMETAPI_LIVE_REQUEST_TIMEOUT_MS: "60000"
COMETAPI_LIVE_CONCURRENCY: "1"
# The test performs exactly three sequential requests (Models, Chat
# non-streaming, Responses streaming), stops on the first failure, and
# The test performs exactly three sequential logical requests (Models,
# Chat non-streaming, Responses streaming), permits one global official
# transient retry, stops on the first logical failure, and
# never prints the credential.
run: npm run test:live
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ jobs:
COMETAPI_KEY: ${{ secrets.COMETAPI_KEY }}
COMETAPI_LIVE_SMOKE: "1"
COMETAPI_SMOKE_MODEL: ${{ vars.COMETAPI_SMOKE_MODEL || 'gpt-5.6-sol' }}
COMETAPI_LIVE_RETRY_BUDGET: "0"
COMETAPI_LIVE_REQUEST_LIMIT: "3"
COMETAPI_LIVE_MAX_OUTPUT_TOKENS: "16"
COMETAPI_LIVE_REQUEST_TIMEOUT_MS: "60000"
Expand Down
17 changes: 10 additions & 7 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,13 @@ The repository maintains four independently auditable workflows:
and 24, with Node.js 26 advisory.
- `live-smoke.yml`: trusted scheduled and manual default-branch-only low-cost
compatibility tests against the pinned CometAPI HTTPS endpoint. Each run makes
exactly three sequential requests, caps generated output at 16 tokens, applies
a 60-second timeout to each request, fixes concurrency at one, and stops on the
first failure. Standalone and release smoke jobs share one repository-wide
concurrency group. Scheduled and manual live execution requires
`LIVE_SMOKE_ENABLED=true`.
exactly three sequential logical requests, permits one global official
transient retry, caps total HTTP attempts at four, caps generated output at
16 tokens, applies a 60-second timeout to each request, fixes concurrency at
one, and stops on the first logical failure. Standalone and release smoke jobs
share one repository-wide concurrency group. Scheduled and manual live
execution requires `LIVE_SMOKE_ENABLED=true`. The release-tag smoke sets the
retry budget to zero and retains a three-attempt limit.
- `release-please.yml`: patch-only versioning, an explicit `cometapi` branch
component, separate pull requests, and the pinned root-package title
`chore(main): release <version>`. It requires
Expand Down Expand Up @@ -414,8 +416,9 @@ dist-tag. Before authorizing release:
immutable GitHub prerelease.
9. Let the release workflow verify `immutable: true`, `main` ancestry, the
exact tarball, and the trusted release-tag live smoke: exactly three
sequential requests, 16 output tokens, 60 seconds per request, concurrency
one, and stop on the first failure.
sequential logical requests, a zero retry budget, three HTTP attempts, 16
output tokens, 60 seconds per request, concurrency one, and stop on the
first logical failure.
10. Approve publication of that same tarball through the protected `npm`
environment with OIDC and provenance, or use the one-time alpha.1 bootstrap
only when normal Trusted Publishing cannot create the new package. The
Expand Down
12 changes: 8 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,9 @@ The readiness criteria remain:
requires approval by the current release approver and permits self-review.
- Default-branch CI is rerun successfully, the fail-closed content gate reports
all violations together and then passes, and an explicitly authorized
protected live smoke passes within the three-request, 16-output-token,
60-second-per-request, concurrency-one, stop-on-first-failure budget.
protected live smoke passes within the three-logical-request, one-global-
retry, four-HTTP-attempt, 16-output-token, 60-second-per-request,
concurrency-one, stop-on-first-logical-failure budget.

## 0.1.0-alpha.1: Registry Alpha

Expand Down Expand Up @@ -432,8 +433,11 @@ The repository will maintain:
- `ci.yml` for offline pull-request and default-branch checks.
- `live-smoke.yml` for budgeted trusted compatibility tests from the default
branch to the pinned CometAPI HTTPS endpoint. Each run is fixed at exactly
three sequential requests, a 16-token output cap, a 60-second per-request
timeout, concurrency one, and stop on the first failure.
three sequential logical requests, permits one global official transient
retry, caps total HTTP attempts at four, uses a 16-token output cap and a
60-second per-request timeout, fixes concurrency at one, and stops on the
first logical failure. The release-tag smoke sets the retry budget to zero
and retains a three-attempt limit.
- `release-please.yml` for the reviewed version and changelog PR and the
corresponding immutable tag and GitHub Release.
- `publish.yml` for trusted Release Please completion, immutable-release and
Expand Down
194 changes: 149 additions & 45 deletions scripts/live-smoke.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import assert from "node:assert/strict";
import {
APIConnectionError,
APIConnectionTimeoutError,
APIError,
} from "openai";

import { CometAPI } from "../dist/index.js";

Expand All @@ -9,17 +14,19 @@ const ALLOWED_URLS = new Set([
`${LIVE_BASE_URL}/responses`,
]);

function readBudget(name, maximum) {
function readBudget(name, maximum, minimum = 1) {
const value = Number(process.env[name] ?? maximum);
if (!Number.isInteger(value) || value < 1 || value > maximum) {
if (!Number.isInteger(value) || value < minimum || value > maximum) {
throw new Error(
`${name} must be an integer between 1 and ${String(maximum)}.`,
`${name} must be an integer between ${String(minimum)} and ${String(maximum)}.`,
);
}
return value;
}

const REQUEST_LIMIT = readBudget("COMETAPI_LIVE_REQUEST_LIMIT", 3);
const LOGICAL_REQUEST_COUNT = 3;
const RETRY_BUDGET = readBudget("COMETAPI_LIVE_RETRY_BUDGET", 1, 0);
const REQUEST_LIMIT = readBudget("COMETAPI_LIVE_REQUEST_LIMIT", 4);
const OUTPUT_TOKEN_LIMIT = readBudget("COMETAPI_LIVE_MAX_OUTPUT_TOKENS", 16);
const REQUEST_TIMEOUT_MS = readBudget(
"COMETAPI_LIVE_REQUEST_TIMEOUT_MS",
Expand All @@ -35,8 +42,13 @@ if (
throw new Error(`The live smoke endpoint is pinned to ${LIVE_BASE_URL}.`);
}

if (REQUEST_LIMIT !== 3 || concurrency !== 1) {
throw new Error("The 0.1 live smoke requires exactly 3 sequential requests.");
if (
REQUEST_LIMIT !== LOGICAL_REQUEST_COUNT + RETRY_BUDGET ||
concurrency !== 1
) {
throw new Error(
"The 0.1 live smoke requires 3 sequential requests and a request limit equal to the logical count plus the global retry budget.",
);
}

if (process.env.COMETAPI_LIVE_SMOKE !== "1") {
Expand All @@ -52,6 +64,89 @@ if (!process.env.COMETAPI_KEY) {
}

let requestCount = 0;
let retryBudget = RETRY_BUDGET;

function classifyError(error) {
let category = "unknown";
let status = null;
if (error instanceof APIConnectionTimeoutError) {
category = "timeout";
} else if (error instanceof APIConnectionError) {
category = "connection";
}
if (
category === "unknown" &&
error instanceof APIError &&
Number.isInteger(error.status)
) {
status = error.status;
category = "http";
}
if (error instanceof globalThis.DOMException && error.name === "AbortError") {
category = "abort";
}
if (error instanceof Error) {
const code = error.code ?? error.cause?.code;
if (
typeof code === "string" &&
[
"ECONNABORTED",
"ECONNREFUSED",
"ECONNRESET",
"EAI_AGAIN",
"ENETUNREACH",
"ENOTFOUND",
"ETIMEDOUT",
].includes(code)
) {
category = "transport";
}
}
const transient =
category === "timeout" ||
category === "connection" ||
category === "abort" ||
category === "transport" ||
(category === "http" &&
(status === 408 || status === 429 || (status !== null && status >= 500)));
return { category, status, transient };
}

async function runLogicalRequest(label, operation) {
for (let attempt = 1; attempt <= 2; attempt += 1) {
const startedAt = Date.now();
try {
const result = await operation();
console.error(
JSON.stringify({
attempt,
category: "success",
elapsed_ms: Date.now() - startedAt,
request: label,
status: null,
}),
);
return result;
} catch (error) {
const classification = classifyError(error);
console.error(
JSON.stringify({
attempt,
category: classification.category,
elapsed_ms: Date.now() - startedAt,
request: label,
status: classification.status,
}),
);
if (retryBudget === 0 || !classification.transient) {
throw error;
}
retryBudget -= 1;
}
}
throw new Error("The live smoke retry loop exhausted unexpectedly.");
}

const guardedFetch = async (input, init) => {
requestCount += 1;
if (requestCount > REQUEST_LIMIT) {
Expand Down Expand Up @@ -82,15 +177,17 @@ const client = new CometAPI({
timeout: REQUEST_TIMEOUT_MS,
});

const models = await client.models.list();
const models = await runLogicalRequest("Models", () => client.models.list());
assert.ok(models.data.length > 0, "models.list returned no models");

const chat = await client.chat.completions.create({
max_completion_tokens: OUTPUT_TOKEN_LIMIT,
messages: [{ content: "Reply with OK.", role: "user" }],
model,
reasoning_effort: "none",
});
const chat = await runLogicalRequest("Chat Completions", () =>
client.chat.completions.create({
max_completion_tokens: OUTPUT_TOKEN_LIMIT,
messages: [{ content: "Reply with OK.", role: "user" }],
model,
reasoning_effort: "none",
}),
);
assert.ok(chat.id, "Chat Completions returned no response ID");
const chatChoice = chat.choices[0];
assert.ok(chatChoice, "Chat Completions returned no choices");
Expand All @@ -104,41 +201,48 @@ assert.ok(
"Chat Completions returned no assistant text",
);

const stream = await client.responses.create({
input: "Reply with OK.",
max_output_tokens: OUTPUT_TOKEN_LIMIT,
model,
reasoning: { effort: "none" },
stream: true,
});
let eventCount = 0;
let responseCompleted = false;
let responseText = "";
for await (const event of stream) {
eventCount += 1;
if (
event.type === "error" ||
event.type === "response.failed" ||
event.type === "response.incomplete"
) {
throw new Error(`Responses stream ended with ${event.type}.`);
const response = await runLogicalRequest("Responses", async () => {
const stream = await client.responses.create({
input: "Reply with OK.",
max_output_tokens: OUTPUT_TOKEN_LIMIT,
model,
reasoning: { effort: "none" },
stream: true,
});
let eventCount = 0;
let responseCompleted = false;
let responseText = "";
for await (const event of stream) {
eventCount += 1;
if (
event.type === "error" ||
event.type === "response.failed" ||
event.type === "response.incomplete"
) {
throw new Error(`Responses stream ended with ${event.type}.`);
}
if (event.type === "response.output_text.delta") {
responseText += event.delta;
}
if (event.type === "response.completed") {
assert.equal(
event.response.status,
"completed",
"Responses terminal event was not completed",
);
responseCompleted = true;
}
}
if (event.type === "response.output_text.delta") {
responseText += event.delta;
}
if (event.type === "response.completed") {
assert.equal(
event.response.status,
"completed",
"Responses terminal event was not completed",
);
responseCompleted = true;
}
}
return { eventCount, responseCompleted, responseText };
});
const { eventCount, responseCompleted, responseText } = response;
assert.ok(eventCount > 0, "Responses stream returned no events");
assert.ok(responseCompleted, "Responses stream returned no completed event");
assert.ok(responseText.trim(), "Responses stream returned no output text");
assert.equal(requestCount, REQUEST_LIMIT);
assert.ok(
requestCount >= LOGICAL_REQUEST_COUNT && requestCount <= REQUEST_LIMIT,
`Live smoke used ${String(requestCount)} HTTP attempts for ${String(LOGICAL_REQUEST_COUNT)} logical requests.`,
);
console.log(
`Live smoke passed ${String(requestCount)} sequential requests with a ${String(OUTPUT_TOKEN_LIMIT)}-token output cap.`,
`Live smoke passed ${String(LOGICAL_REQUEST_COUNT)} sequential requests in ${String(requestCount)} HTTP attempts with a ${String(OUTPUT_TOKEN_LIMIT)}-token output cap.`,
);
3 changes: 2 additions & 1 deletion scripts/release-workflow-validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const RELEASE_WORKFLOW_STEP = "Run Release Please";
const PUBLISH_OPERATION = "release";
const NPM_TAG_POLICY_ID = 55718965;
const PUBLISH_WORKFLOW_CONTRACT_SHA256 =
"081af3a47c0bef1ff38a77cf038ab3c66b8c14ea2b524c42bc4f2c21e41f9f79";
"684e15c13835ae3b139929ae78e5bbd5ba618ea8f30ebba8dbb4c42004d57a06";
const PUBLISH_HANDOFF_IF =
"vars.RELEASE_PLEASE_ENABLED == 'true' && github.event_name == 'workflow_run' && github.run_attempt == 1 && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main'";
const PUBLISH_RESULT_IF = "steps.result.outputs.has-result == 'true'";
Expand Down Expand Up @@ -570,6 +570,7 @@ fi
COMETAPI_KEY: "${{ secrets.COMETAPI_KEY }}",
COMETAPI_LIVE_SMOKE: "1",
COMETAPI_SMOKE_MODEL: "${{ vars.COMETAPI_SMOKE_MODEL || 'gpt-5.6-sol' }}",
COMETAPI_LIVE_RETRY_BUDGET: "0",
COMETAPI_LIVE_REQUEST_LIMIT: "3",
COMETAPI_LIVE_MAX_OUTPUT_TOKENS: "16",
COMETAPI_LIVE_REQUEST_TIMEOUT_MS: "60000",
Expand Down
Loading
Loading