diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e6d29f..e434fa01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Switched the repository-local OpenCode development configuration from GitHub Models to an NVIDIA NIM-only candidate set while preserving organization-level review-workflow ownership in `ContextualWisdomLab/.github`. +- Production planning-analysis requests now combine tenant-bound, server-derived + contextual-orchestrator cost attribution with explicit `auto` orchestration + mode, delegating provider/model/topology policy to the shared service without + weakening ScopeWeave's authenticated, fail-closed transport or response + boundary controls. - Accepted XML whitespace before exact Microsoft Project element delimiters while preserving the linear, regex-free import scanner and rejecting attributes, longer names, non-XML whitespace, nested unmatched blocks, and diff --git a/docs/doctoring/contextual-orchestrator-auto-default.md b/docs/doctoring/contextual-orchestrator-auto-default.md new file mode 100644 index 00000000..c3d5d2f5 --- /dev/null +++ b/docs/doctoring/contextual-orchestrator-auto-default.md @@ -0,0 +1,41 @@ +# Contextual-orchestrator adaptive planning default + +## Status + +Active pull-request evidence. This record does not describe protected `develop` until the owning pull request is integrated. + +## Decision boundary + +ScopeWeave owns the meaning, authorization, cost attribution, and presentation of a planning-analysis request. The shared `contextual-orchestrator` service owns provider/model selection and the depth/topology of execution. Production ScopeWeave requests therefore send `orchestration_mode: "auto"` explicitly instead of relying on an implicit gateway default or selecting `route`/`conduct` locally. + +The binding dependency evidence verified for this slice is protected `ContextualWisdomLab/contextual-orchestrator` `main` commit `6841b71935e0b7cb98fb52bcb4709cc5100c8d87`. At that revision, `/v1/chat/completions` accepts `orchestration_mode`, permits `auto`, `route`, and `conduct`, accepts bounded attribution metadata, and routes execution through the orchestrator rather than treating the request model label as a provider lock. + +This decision does **not** promise a specific provider, model, worker count, topology, verifier strategy, or cost heuristic. Those remain shared-service policy and may evolve behind its versioned contract. + +## Attribution and tenant authority + +Authenticated project AI briefings attach `service=scopeweave` and the project organization as `account` only after membership-scoped project authorization. Browser request fields cannot select another tenant's accounting identity. The client forwards only supported attribution dimensions, accepts bounded strings or finite numeric identifiers, uses a prototype-free validated map, and omits empty attribution. These labels are accounting metadata and never grant execution-provider or model-selection authority. + +## Security and standalone behavior + +The change preserves the protected ScopeWeave orchestrator boundary: authenticated canonical provider origin, HTTPS outside explicit loopback development, bounded messages, 120-second request timeout, bounded streamed provider responses, sanitized failures, and deterministic text only under explicit `SCOPEWEAVE_DEV=1` development mode. No provider credential or caller-controlled execution policy is added. + +## TDD and overlap-convergence evidence + +The adaptive-mode work originally existed separately in PR #529 while cost attribution occupied the same production request-body boundary in PR #496. Keeping both as independent roots created a concrete future regression risk: whichever branch integrated second could erase the other request field. The older attribution owner was therefore made the canonical combined boundary rather than allowing two competing implementations. + +On the canonical branch, test-only commits `dc71cdff9dc258b8f196c35d9b92c1542e869043` and `5510058ae7437ede44fb7a7fd94351ac7f7d6b14` first require `orchestration_mode: "auto"` both on ordinary hardened requests and while tenant-bound attribution is present or omitted. Source commit `bd8878591bfa74b67ae2a36b122513d2c41e376f` then composes adaptive routing with the existing sanitized attribution request. Exact-current-head hosted evidence remains authoritative; predecessor checks are not reused. + +## Rollback + +Rollback of adaptive mode removes the explicit `orchestration_mode` field and its matching regression/documentation while preserving the tenant-bound attribution and hardened transport. Rollback of attribution separately removes only the attribution call-site, sanitizer, and attribution regressions. Neither rollback may restore stale pre-hardening orchestrator source or a self-modifying workflow. + +## APA 7th references + +Contextual Wisdom Lab. (2026). *contextual-orchestrator* (Commit 6841b71935e0b7cb98fb52bcb4709cc5100c8d87) [Computer software]. GitHub. + +Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor*. arXiv. https://doi.org/10.48550/arXiv.2512.04388 + +Sakana AI. (2026). *Sakana Fugu: Multi-agent system as a model*. https://sakana.ai/fugu/ + +Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator*. arXiv. https://doi.org/10.48550/arXiv.2512.04695 diff --git a/docs/orchestrator-production.md b/docs/orchestrator-production.md index c2c4c5c7..e090a17b 100644 --- a/docs/orchestrator-production.md +++ b/docs/orchestrator-production.md @@ -38,6 +38,28 @@ endpoint is absent. That variable must never be set in staging or production. ## Orchestration responsibility +ScopeWeave explicitly sends `orchestration_mode: "auto"` together with the +configured model and validated messages on production briefing requests. The +current protected `ContextualWisdomLab/contextual-orchestrator` `main` contract +verified for this change, commit +`6841b71935e0b7cb98fb52bcb4709cc5100c8d87`, accepts `auto`, `route`, and +`conduct` as orchestration modes. ScopeWeave chooses `auto` as its default so +execution policy can be optimized centrally without coupling this product to a +specific provider, worker count, topology, verifier pattern, or cost heuristic. +Those internal choices remain `contextual-orchestrator` authority and are not a +ScopeWeave compatibility promise. + +For authenticated project AI briefings, ScopeWeave also sends bounded business +cost attribution derived from server-side project state. `service=scopeweave` +and the authenticated project organization `account` are attached only after +membership-scoped project access succeeds. Caller payload fields cannot choose +another tenant's attribution. The client forwards only the orchestration +service's supported attribution dimensions, accepts only bounded string or +finite numeric values, holds validated labels in a prototype-free map, and +omits the attribution object entirely when no valid labels remain. Attribution +is accounting metadata only: it cannot select an execution provider, model, or +orchestration topology. + ScopeWeave intentionally sends only a versioned OpenAI-compatible request to the orchestration service. Model selection, single-model versus multi-agent allocation, task decomposition, role-specific reasoning effort, recursion diff --git a/package.json b/package.json index dc32b12b..7aed744a 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "check:python-docstrings": "node scripts/ci/static_coverage_evidence.mjs docstrings", "coverage": "npm run test:coverage", "server": "node server/server.mjs", - "test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs", - "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs", + "test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs", + "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs", "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases", - "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api", + "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed", "test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js tests/e2e/toast-accessibility.spec.js", diff --git a/server/app.mjs b/server/app.mjs index 03908830..c432a84f 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -995,7 +995,10 @@ app.post('/api/projects/:id/ai/brief', requireAuth, async (c) => { const analysis = await orchestratorChat([ { role: 'system', content: '너는 공정관리(schedule control) 전문가다. 주어진 프로젝트 지표를 근거로 한국어 경영진 브리핑을 작성하라: ①일정 상태 한 줄 판정 ②핵심 리스크 2~3개(근거 지표 인용) ③실행 권고 2~3개. 지표에 없는 사실은 만들지 마라.' }, { role: 'user', content: context }, - ]); + ], { + service: 'scopeweave', + account: String(p.org_id), + }); logAudit(p.org_id, uid, 'ai.brief', 'project', p.id, { tasks: tasks.length }); return c.json({ analysis }); } catch (e) { diff --git a/server/orchestrator.mjs b/server/orchestrator.mjs index b3e8e400..fccf23d0 100644 --- a/server/orchestrator.mjs +++ b/server/orchestrator.mjs @@ -9,6 +9,17 @@ const MAX_CONTENT_LENGTH = 100_000; const MAX_PROVIDER_RESPONSE_BYTES = 1024 * 1024; // WHATWG URL serializes an IPv6 hostname with brackets (`[::1]`). const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']); +const MAX_ATTRIBUTION_VALUE_LENGTH = 256; +const ATTRIBUTION_DIMENSIONS = new Set([ + 'account', + 'service', + 'upstream_api', + 'model_name', + 'team', + 'group', + 'company', + 'provider', +]); export const orchestratorMock = process.env.SCOPEWEAVE_DEV === '1' && !OC_URL; @@ -137,6 +148,55 @@ function validatedMessages(messages) { }); } +/** + * Copy optional cost-attribution labels into the exact orchestrator allowlist. + * + * Unknown dimensions and empty values are omitted rather than forwarded to the + * strict contextual-orchestrator request validator. Values must be strings or + * finite numeric identifiers before normalization to bounded strings; complex + * objects and non-finite numbers fail closed instead of becoming misleading + * labels through implicit JavaScript string coercion. Execution model/provider + * identity remains controlled by the top-level request model and the + * orchestrator's own provider routing evidence; this object is business + * cost-allocation metadata only. + * + * @param {unknown} attribution optional business cost-attribution mapping + * @returns {Record|undefined} bounded allowed labels or undefined + */ +function sanitizedAttribution(attribution) { + if (attribution === undefined || attribution === null) return undefined; + if (typeof attribution !== 'object' || Array.isArray(attribution)) { + throw new OrchestratorConfigurationError( + 'orchestrator_attribution_invalid', + 'Orchestrator attribution must be an object when provided.', + ); + } + + const safe = Object.create(null); + for (const [key, value] of Object.entries(attribution)) { + if (!ATTRIBUTION_DIMENSIONS.has(key) || value === undefined || value === null) continue; + if ( + typeof value !== 'string' + && (typeof value !== 'number' || !Number.isFinite(value)) + ) { + throw new OrchestratorConfigurationError( + 'orchestrator_attribution_invalid', + 'Orchestrator attribution values must be strings or finite numbers.', + ); + } + const text = String(value).trim(); + if (!text) continue; + if (text.length > MAX_ATTRIBUTION_VALUE_LENGTH) { + throw new OrchestratorConfigurationError( + 'orchestrator_attribution_invalid', + 'Orchestrator attribution value is outside the accepted boundary.', + ); + } + safe[key] = text; + } + return Object.keys(safe).length ? safe : undefined; +} + /** * Build the stable response-size failure used by declared and streamed limits. * @returns {OrchestratorConfigurationError} Operator-safe size error. @@ -275,11 +335,13 @@ async function rejectProviderResponse(response) { /** * Generate one AI briefing through contextual-orchestrator. * @param {unknown} messages OpenAI-compatible messages + * @param {unknown} [attribution] optional bounded business cost-attribution labels * @returns {Promise} */ -export async function chat(messages) { +export async function chat(messages, attribution) { const configuration = orchestratorConfiguration(); const safeMessages = validatedMessages(messages); + const safeAttribution = sanitizedAttribution(attribution); if (configuration.mock) { const user = safeMessages .filter((message) => message.role === 'user') @@ -303,7 +365,12 @@ export async function chat(messages) { 'content-type': 'application/json', authorization: `Bearer ${configuration.token}`, }, - body: JSON.stringify({ model: OC_MODEL, messages: safeMessages }), + body: JSON.stringify({ + model: OC_MODEL, + orchestration_mode: 'auto', + messages: safeMessages, + ...(safeAttribution ? { attribution: safeAttribution } : {}), + }), signal: AbortSignal.timeout(ORCHESTRATOR_TIMEOUT_MS), }); } catch { diff --git a/tests/api/orchestrator-attribution.test.mjs b/tests/api/orchestrator-attribution.test.mjs new file mode 100644 index 00000000..d07460a3 --- /dev/null +++ b/tests/api/orchestrator-attribution.test.mjs @@ -0,0 +1,89 @@ +import assert from 'node:assert/strict'; + +process.env.SCOPEWEAVE_DB = ':memory:'; +process.env.SCOPEWEAVE_JWT_SECRET = '0123456789abcdef0123456789abcdef'; +delete process.env.SCOPEWEAVE_DEV; +process.env.ORCHESTRATOR_URL = 'https://orchestrator.example'; +process.env.ORCHESTRATOR_TOKEN = 'secret-token'; +process.env.ORCHESTRATOR_MODEL = 'nvidia/nemotron-3-super-120b-a12b'; + +const providerCalls = []; +globalThis.fetch = async (url, init) => { + providerCalls.push({ url: String(url), init }); + return new Response(JSON.stringify({ + choices: [{ message: { content: 'Grounded production response' } }], + }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); +}; + +const { app } = await import(`../../server/app.mjs?attribution-api-test=${Date.now()}`); + +const jsonRequest = (path, options = {}) => app.request(path, { + ...options, + headers: { + 'content-type': 'application/json', + ...(options.headers || {}), + }, +}); +const jsonBody = (value) => JSON.stringify(value); + +async function createAccount(email) { + let response = await jsonRequest('/api/auth/signup', { + method: 'POST', + body: jsonBody({ email, password: 'password123', name: email }), + }); + assert.equal(response.status, 200, `${email} signup`); + const token = (await response.json()).token; + const auth = { authorization: `Bearer ${token}` }; + response = await jsonRequest('/api/me', { headers: auth }); + assert.equal(response.status, 200, `${email} account lookup`); + const account = await response.json(); + return { auth, orgId: account.orgs[0].id }; +} + +const owner = await createAccount('orchestrator-owner@scopeweave.test'); +const outsider = await createAccount('orchestrator-outsider@scopeweave.test'); + +let response = await jsonRequest('/api/projects', { + method: 'POST', + headers: owner.auth, + body: jsonBody({ name: 'Attribution Project' }), +}); +assert.equal(response.status, 200, 'owner creates attribution project'); +const projectId = (await response.json()).id; + +response = await jsonRequest(`/api/projects/${projectId}/ai/brief`, { + method: 'POST', + headers: owner.auth, + body: jsonBody({ account: String(outsider.orgId), service: 'spoofed-client-service' }), +}); +assert.equal(response.status, 200, 'authorized owner receives AI briefing'); +assert.equal(providerCalls.length, 1, 'authorized briefing performs one provider call'); +assert.equal(providerCalls[0].url, 'https://orchestrator.example/v1/chat/completions'); +const providerBody = JSON.parse(providerCalls[0].init.body); +assert.deepEqual( + providerBody.attribution, + { service: 'scopeweave', account: String(owner.orgId) }, + 'the authenticated server-side project organization owns cost attribution', +); +assert.notEqual( + providerBody.attribution.account, + String(outsider.orgId), + 'browser-supplied account data cannot spoof another tenant attribution', +); + +response = await jsonRequest(`/api/projects/${projectId}/ai/brief`, { + method: 'POST', + headers: outsider.auth, + body: jsonBody({ account: String(owner.orgId) }), +}); +assert.equal(response.status, 404, 'cross-tenant AI briefing hides project existence'); +assert.equal( + providerCalls.length, + 1, + 'cross-tenant requests are rejected before any contextual-orchestrator call', +); + +console.log('✓ AI briefing attribution tenant-boundary tests passed'); diff --git a/tests/unit/orchestrator-attribution.test.mjs b/tests/unit/orchestrator-attribution.test.mjs new file mode 100644 index 00000000..45934f6e --- /dev/null +++ b/tests/unit/orchestrator-attribution.test.mjs @@ -0,0 +1,117 @@ +import assert from 'node:assert/strict'; + +process.env.SCOPEWEAVE_DEV = ''; +process.env.ORCHESTRATOR_URL = 'https://orchestrator.example'; +process.env.ORCHESTRATOR_TOKEN = 'secret-token'; +process.env.ORCHESTRATOR_MODEL = 'nvidia/nemotron-3-super-120b-a12b'; + +const calls = []; +globalThis.fetch = async (url, init) => { + calls.push({ url, init }); + return new Response(JSON.stringify({ + choices: [{ message: { content: 'Grounded production response' } }], + }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); +}; + +const { chat } = await import( + `../../server/orchestrator.mjs?attribution-test=${Date.now()}-${Math.random()}` +); + +const messages = [{ role: 'user', content: 'status' }]; + +assert.equal( + await chat(messages, { + service: 'scopeweave', + account: 42, + upstream_api: 'requested-upstream-label', + provider: 'requested-provider-label', + model_name: 'requested-model-label', + team: null, + group: '', + company: ' ', + unsupported_dimension: 'must-not-cross-boundary', + }), + 'Grounded production response', +); + +assert.equal(calls.length, 1); +const attributedBody = JSON.parse(calls[0].init.body); +assert.equal(attributedBody.model, 'nvidia/nemotron-3-super-120b-a12b'); +assert.equal(attributedBody.orchestration_mode, 'auto'); +assert.equal(Object.hasOwn(attributedBody, 'provider'), false); +assert.deepEqual(attributedBody.attribution, { + service: 'scopeweave', + account: '42', + upstream_api: 'requested-upstream-label', + provider: 'requested-provider-label', + model_name: 'requested-model-label', +}); +assert.equal( + Object.hasOwn(attributedBody.attribution, 'unsupported_dimension'), + false, + 'unknown attribution keys never cross the ScopeWeave boundary', +); + +await chat(messages, { unsupported_dimension: 'x', account: ' ' }); +const emptyBody = JSON.parse(calls[1].init.body); +assert.equal(emptyBody.orchestration_mode, 'auto'); +assert.equal( + Object.hasOwn(emptyBody, 'attribution'), + false, + 'an attribution field is omitted when no non-empty allowed dimensions remain', +); + +await chat(messages); +const legacyBody = JSON.parse(calls[2].init.body); +assert.deepEqual( + legacyBody, + { + model: 'nvidia/nemotron-3-super-120b-a12b', + orchestration_mode: 'auto', + messages, + }, + 'omitting attribution preserves the hardened adaptive request shape exactly', +); + +for (const invalidAttribution of [ + [], + 'scopeweave', + { service: 'x'.repeat(257) }, + { service: ['scopeweave'] }, + { account: { organization_id: 42 } }, + { team: Symbol('scopeweave') }, + { group: Number.NaN }, + { company: Number.POSITIVE_INFINITY }, +]) { + await assert.rejects( + chat(messages, invalidAttribution), + (error) => error.code === 'orchestrator_attribution_invalid', + 'malformed, non-scalar, non-finite, or unbounded attribution fails before provider transport', + ); +} +assert.equal(calls.length, 3, 'invalid attribution never reaches the provider'); + +const originalJsonStringify = JSON.stringify; +let serializedAttributionPrototype; +JSON.stringify = (value, ...args) => { + if (value?.attribution) { + serializedAttributionPrototype = Object.getPrototypeOf(value.attribution); + } + return originalJsonStringify(value, ...args); +}; +try { + await chat(messages, { service: 'scopeweave' }); +} finally { + JSON.stringify = originalJsonStringify; +} +assert.equal( + serializedAttributionPrototype, + null, + 'validated attribution is held in a prototype-free map before provider serialization', +); +assert.equal(calls.length, 4, 'prototype-free attribution still reaches the provider once'); + +console.log('✓ orchestrator attribution boundary tests passed'); \ No newline at end of file diff --git a/tests/unit/orchestrator.test.mjs b/tests/unit/orchestrator.test.mjs index 14de7136..87cfb647 100644 --- a/tests/unit/orchestrator.test.mjs +++ b/tests/unit/orchestrator.test.mjs @@ -98,6 +98,7 @@ try { assert.ok(calls[0].init.signal instanceof AbortSignal); assert.deepEqual(JSON.parse(calls[0].init.body), { model: 'nvidia/nemotron-3-super-120b-a12b', + orchestration_mode: 'auto', messages: [{ role: 'user', content: 'status' }], });