@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -4,6 +4,7 @@ import {actualCost} from '../../../.dsh-plugin/shared/run-ledger.mjs'
|
|
|
4
4
|
import {managedModelRequest,taskModelRequest} from './model-receipt.mjs'
|
|
5
5
|
import {assessFundingAdmission} from './funding-admission.mjs'
|
|
6
6
|
import {taskHash} from './task-contract.mjs'
|
|
7
|
+
import {planOutputBound,requestFieldAccess} from './model-request-bounds.mjs'
|
|
7
8
|
|
|
8
9
|
const fail=code=>{throw Object.assign(new Error(code),{code})}
|
|
9
10
|
const deferred=()=>{let resolve;const promise=new Promise(r=>{resolve=r});return {promise,resolve}}
|
|
@@ -25,7 +26,7 @@ function usageOf(value){
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/** Every observed model call has one source receipt; only the linked run owns money. */
|
|
28
|
-
export function createModelAccounting({store,authority,pricingFor,inputBoundFor,machineId,workspaceId,now=Date.now,ttlMs=120000,limits={dailyLimitUsd:1,monthlyLimitUsd:0},maxActive=16}){
|
|
29
|
+
export function createModelAccounting({store,authority,pricingFor,fundingRevisionFor,inputBoundFor,modelBoundsFor,preparedFrameFor,machineId,workspaceId,now=Date.now,ttlMs=120000,limits={dailyLimitUsd:1,monthlyLimitUsd:0},maxActive=16}){
|
|
29
30
|
if(store?.kind!=='sqlite-router-state'||typeof store.recordModelReceipt!=='function'||typeof pricingFor!=='function')fail('MODEL_ACCOUNTING_STORAGE_REQUIRED')
|
|
30
31
|
const active=new Set();let closed=false
|
|
31
32
|
const controllerSet=new Set()
|
|
@@ -37,7 +38,8 @@ export function createModelAccounting({store,authority,pricingFor,inputBoundFor,
|
|
|
37
38
|
...(policy.scopeId?{fundingScopeId:policy.scopeId}:{}),...(policy.projectId?{fundingProjectId:policy.projectId}:{}),
|
|
38
39
|
requestHash:requestHash(options),createdAt,ownerPid:process.pid,state:'admitted',finishedAt:null,usage:null,usageComplete:false,actualModel:null,reportedCostUsd:null,finishKind:null,outcome:null}
|
|
39
40
|
}
|
|
40
|
-
const callStore=(receipt,config,estimate)=>({read:()=>store.read(),update:(change,options)=>store.update(current=>{
|
|
41
|
+
const callStore=(receipt,config,estimate,assertFunding)=>({read:()=>store.read(),update:(change,options)=>store.update(current=>{
|
|
42
|
+
if (receipt.state === 'admitted' || receipt.state === 'running') assertFunding()
|
|
41
43
|
if(receipt.state==='admitted'&&config.funding==='api'){
|
|
42
44
|
const checked=assessFundingAdmission({store,state:current,botId:receipt.botId,scopeId:config.scopeId,projectId:config.projectId,policy:config,estimatedCostUsd:estimate,excludeRunId:receipt.runId,now:now()})
|
|
43
45
|
if(!checked.accepted)return checked
|
|
@@ -65,30 +67,92 @@ export function createModelAccounting({store,authority,pricingFor,inputBoundFor,
|
|
|
65
67
|
authority.assertTool(identity.subject,'model/infer')
|
|
66
68
|
if(authority.describe(identity.subject).botId!==identity.botId)fail('MODEL_OWNER_MISMATCH')
|
|
67
69
|
if(task&&(task.botId!==identity.botId||task.provider!==options.provider||task.model!==options.model))fail('MODEL_TASK_OWNER_MISMATCH')
|
|
68
|
-
const
|
|
70
|
+
const fundingQuery={botId:identity.botId,provider:options.provider,model:options.model,source:identity.source}
|
|
71
|
+
const config=await pricingFor(fundingQuery)
|
|
69
72
|
if(active.size>=maxActive||active.has(options))fail('MODEL_CONCURRENCY_LIMIT')
|
|
70
73
|
if(!config||!['api','subscription'].includes(config.funding))fail('MODEL_FUNDING_REQUIRED')
|
|
71
74
|
if(config.funding==='api'&&(!config.pricing||!['input','output'].every(k=>Number.isFinite(config.pricing[k])&&config.pricing[k]>=0)))fail('MODEL_PRICE_REQUIRED')
|
|
72
|
-
if(options.maxTokens!==undefined&&
|
|
75
|
+
if(options.maxTokens!==undefined&&(!Number.isSafeInteger(options.maxTokens)||options.maxTokens<1))fail('MODEL_OUTPUT_BOUNDARY_REQUIRED')
|
|
73
76
|
if(config.funding==='api'&&(!Number.isSafeInteger(config.maxOutputTokens)||config.maxOutputTokens<1))fail('MODEL_ESTIMATE_REQUIRED')
|
|
74
|
-
// A declared request allowance is not a
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
+
// A declared request allowance is a price, not a capacity: the trusted
|
|
78
|
+
// input upper bound is the pinned model's own context window, resolved
|
|
79
|
+
// per admission because an adapter registration can be replaced under a
|
|
80
|
+
// live Host. That whole window already covers images, tool declarations
|
|
81
|
+
// and adapter framing, so no tokenizer and no source-length estimate
|
|
82
|
+
// stands in for it. A route whose adapter declares no window has no bound
|
|
83
|
+
// to admit, which is a required-bound failure rather than a smaller number
|
|
84
|
+
// to pretend with.
|
|
85
|
+
// A prepared call (the independent path) already bound one adapter generation and
|
|
86
|
+
// recorded its exact capacities in a Host-owned frame keyed by this request's signal.
|
|
87
|
+
// When that frame is present and names this exact route, admission uses it instead of
|
|
88
|
+
// reading the current registration: the window and default it prices are the ones the
|
|
89
|
+
// prepared dispatch will actually stream, so no metadata read can combine one
|
|
90
|
+
// generation's window with another's endpoint. A frame about another route is ignored.
|
|
91
|
+
const frame=preparedFrameFor?await preparedFrameFor(options):null
|
|
92
|
+
if (!managed && (config.fundingRevision || frame?.fundingRevision)) {
|
|
93
|
+
if (!frame || !Object.hasOwn(frame, 'fundingRevision')) fail('MODEL_FUNDING_FRAME_MISSING')
|
|
94
|
+
if (frame.fundingRevision !== (config.fundingRevision ?? null)) fail('MODEL_FUNDING_CHANGED')
|
|
95
|
+
}
|
|
96
|
+
const assertFunding=()=>{
|
|
97
|
+
if (config.fundingRevision && fundingRevisionFor?.(fundingQuery) !== config.fundingRevision) fail('MODEL_FUNDING_CHANGED')
|
|
98
|
+
}
|
|
99
|
+
assertFunding()
|
|
100
|
+
if(closed)fail('MODEL_ACCOUNTING_CLOSED')
|
|
101
|
+
if(active.size>=maxActive||active.has(options))fail('MODEL_CONCURRENCY_LIMIT')
|
|
102
|
+
options.signal?.throwIfAborted()
|
|
103
|
+
const frameBounds=frame&&frame.bounds&&frame.provider===options.provider&&frame.model===options.model?frame.bounds:null
|
|
104
|
+
let outputDefault=null
|
|
77
105
|
if(config.funding==='api'){
|
|
78
|
-
|
|
106
|
+
let inputBound
|
|
107
|
+
if(frameBounds){
|
|
108
|
+
inputBound=frameBounds.contextWindow
|
|
109
|
+
outputDefault=frameBounds.defaultMaxTokens??null
|
|
110
|
+
}else{
|
|
111
|
+
const bounds=modelBoundsFor?await modelBoundsFor({options,identity,config,signal:options.signal??null}):null
|
|
112
|
+
inputBound=bounds?.inputBound??(inputBoundFor?await inputBoundFor({options,identity,config}):null)
|
|
113
|
+
outputDefault=bounds?.outputDefault??null
|
|
114
|
+
}
|
|
79
115
|
if(!Number.isSafeInteger(inputBound)||inputBound<1||!Number.isSafeInteger(config.maxInputTokens)||inputBound>config.maxInputTokens)fail('MODEL_INPUT_BOUNDARY_REQUIRED')
|
|
80
116
|
}
|
|
117
|
+
// The output bound the provider will actually see. An omitted bound is not
|
|
118
|
+
// an unbounded one: the pinned adapter materializes its own per-request
|
|
119
|
+
// cap into the request before dispatch, so that declared cap — and nothing
|
|
120
|
+
// else — is what arrives. A request the Host cannot bring under the
|
|
121
|
+
// allowance, whether frozen above it or declaring no bound at all, is
|
|
122
|
+
// refused here: before a receipt, a reservation, a lease, an active slot or
|
|
123
|
+
// any provider call exists.
|
|
81
124
|
const outputBound=config.maxOutputTokens
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
let plan
|
|
126
|
+
if(frameBounds){
|
|
127
|
+
// The prepared config already carries the dispatched bound (clamped before it
|
|
128
|
+
// froze). Admit it as-is and only validate it against the current allowance; a
|
|
129
|
+
// second clamp here would change the dispatched config and fail the prepared call
|
|
130
|
+
// with INVALID_PREPARED_CALL, so a lowered allowance is a refusal, never a re-clamp.
|
|
131
|
+
const dispatched=options.maxTokens??null
|
|
132
|
+
if(config.funding==='api'){
|
|
133
|
+
if(!Number.isSafeInteger(dispatched)||dispatched<1)fail('MODEL_ESTIMATE_REQUIRED')
|
|
134
|
+
if(dispatched>outputBound)fail('MODEL_OUTPUT_BOUND_EXCEEDED')
|
|
135
|
+
}
|
|
136
|
+
plan={ok:true,maxTokens:options.maxTokens??undefined,source:'prepared'}
|
|
137
|
+
}else{
|
|
138
|
+
plan=Number.isSafeInteger(outputBound)&&outputBound>0
|
|
139
|
+
?planOutputBound({requested:options.maxTokens??null,allowance:outputBound,adapterDefault:outputDefault})
|
|
140
|
+
:{ok:true,maxTokens:options.maxTokens??undefined,source:'none'}
|
|
141
|
+
}
|
|
142
|
+
if(!plan.ok)fail(plan.code)
|
|
143
|
+
// Mutability is per field and independent of every other field's: a request
|
|
144
|
+
// whose signal is read-only may still carry a writable maxTokens.
|
|
145
|
+
const tokenAccess=requestFieldAccess(options,'maxTokens'),signalAccess=requestFieldAccess(options,'signal')
|
|
146
|
+
if(plan.source==='clamp'&&!tokenAccess.canWrite)fail(plan.refusal)
|
|
147
|
+
const effectiveOutput=plan.maxTokens
|
|
148
|
+
// The bounds lookup is asynchronous, so the gates it ran past are re-run
|
|
149
|
+
// here: an accounting that closed while the adapter was being asked, or an
|
|
150
|
+
// active set another admission filled, must not gain a slot because a
|
|
151
|
+
// metadata read was slow.
|
|
152
|
+
if(closed)fail('MODEL_ACCOUNTING_CLOSED')
|
|
153
|
+
if(active.size>=maxActive||active.has(options))fail('MODEL_CONCURRENCY_LIMIT')
|
|
90
154
|
const record=receiptBase(identity,{...options,...(effectiveOutput!==undefined?{maxTokens:effectiveOutput}:{})},config,managed,task)
|
|
91
|
-
let manager,accepted,binding,lease,guard,inner,estimate,previousSignal=options.signal,changedOutput=false,leaseSignal=null
|
|
155
|
+
let manager,accepted,binding,lease,guard,inner,estimate,previousSignal=options.signal,changedOutput=false,changedSignal=false,leaseSignal=null
|
|
92
156
|
let started=false,exhausted=false,terminal=false,finishKind=null,observed={usage:null,complete:false},primaryError,cleanupError,propagated=false,aborted=false,consumerClosed=false
|
|
93
157
|
const controller=new AbortController();controllerSet.add(controller);active.add(options)
|
|
94
158
|
const build=()=>{
|
|
@@ -116,37 +180,35 @@ export function createModelAccounting({store,authority,pricingFor,inputBoundFor,
|
|
|
116
180
|
estimate=record.funding==='subscription'?0:(input*config.pricing.input+output*config.pricing.output)/1e6
|
|
117
181
|
if(!Number.isFinite(estimate))fail('MODEL_ESTIMATE_REQUIRED')
|
|
118
182
|
const pending={id:record.runId,createdAt:record.createdAt,status:'running',finishedAt:null,packages:[],reviews:[],modelCall:{schemaVersion:1,id:record.id,botId:record.botId,source:record.source,...(record.fundingScopeId?{fundingScopeId:record.fundingScopeId}:{}),...(record.fundingProjectId?{fundingProjectId:record.fundingProjectId}:{})}}
|
|
119
|
-
manager=createBudgetReservations({state:callStore(record,config,estimate)})
|
|
183
|
+
manager=createBudgetReservations({state:callStore(record,config,estimate,assertFunding)})
|
|
120
184
|
accepted=await manager.beginExecution({run:pending,packageEstimates:[{id:'inference',billing:record.funding,estimatedCostUsd:estimate}],...limits})
|
|
121
185
|
if(!accepted.accepted)fail(accepted.reason==='subscription-call-limit'?'MODEL_SUBSCRIPTION_LIMIT':'MODEL_WAITING_BUDGET')
|
|
122
186
|
}
|
|
123
187
|
binding={taskId:record.taskId??record.runId,attemptId:record.attemptId,machineId,workspaceId,location:'local'}
|
|
124
188
|
lease=authority.issue(identity.subject,{binding,tools:['model/infer'],ttlMs})
|
|
125
189
|
// The caller request may be frozen (the SDK AgentLoop passes one). A
|
|
126
|
-
//
|
|
127
|
-
//
|
|
190
|
+
// request whose signal field is writable carries the combined lease
|
|
191
|
+
// signal so the provider aborts directly; otherwise the lease gates the
|
|
128
192
|
// iteration instead and cleanup still returns the provider iterator.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
options.maxTokens
|
|
134
|
-
|
|
135
|
-
// Adoption is best effort: the estimate above already uses the full allowance, so a
|
|
136
|
-
// sealed request without an output bound is admitted as it is instead of failing.
|
|
137
|
-
options.maxTokens=effectiveOutput;changedOutput=true
|
|
193
|
+
if(plan.source==='clamp'){
|
|
194
|
+
// Write the bound the provider will see, then read it back: a field
|
|
195
|
+
// that reports writable but does not take the value is not one the
|
|
196
|
+
// Host may claim it clamped.
|
|
197
|
+
if(!Reflect.set(options,'maxTokens',effectiveOutput)||options.maxTokens!==effectiveOutput)fail(plan.refusal)
|
|
198
|
+
changedOutput=true
|
|
138
199
|
}
|
|
139
200
|
guard=authority.run(lease.token,binding,'model/infer',async({signal})=>{
|
|
140
|
-
if(
|
|
201
|
+
if(signalAccess.canWrite){options.signal=AbortSignal.any([signal,...(previousSignal?[previousSignal]:[])]);changedSignal=true;options.signal.throwIfAborted()}
|
|
141
202
|
else{leaseSignal=signal;signal.throwIfAborted()}
|
|
142
203
|
record.state='running'
|
|
143
204
|
if(managed)await store.recordModelReceipt(record)
|
|
144
205
|
else{
|
|
145
|
-
const value=await createBudgetReservations({state:callStore(record,config,estimate)}).checkpointExecution({reservationId:accepted.reservationId,ownerToken:accepted.ownerToken,run:{id:record.runId,createdAt:record.createdAt,status:'running',packages:[],reviews:[]},startedIds:['inference'],...limits,checkBudget:true})
|
|
206
|
+
const value=await createBudgetReservations({state:callStore(record,config,estimate,assertFunding)}).checkpointExecution({reservationId:accepted.reservationId,ownerToken:accepted.ownerToken,run:{id:record.runId,createdAt:record.createdAt,status:'running',packages:[],reviews:[]},startedIds:['inference'],...limits,checkBudget:true})
|
|
146
207
|
if(value.accepted===false)fail('MODEL_WAITING_BUDGET')
|
|
147
208
|
}
|
|
148
209
|
authority.assertModel(identity.subject,options.provider,options.model)
|
|
149
|
-
|
|
210
|
+
assertFunding()
|
|
211
|
+
if(signalAccess.canWrite)options.signal.throwIfAborted();else signal.throwIfAborted()
|
|
150
212
|
inner=next()[Symbol.asyncIterator]();started=true;ready.resolve(true)
|
|
151
213
|
await done.promise
|
|
152
214
|
},{signal:controller.signal})
|
|
@@ -176,15 +238,17 @@ export function createModelAccounting({store,authority,pricingFor,inputBoundFor,
|
|
|
176
238
|
try{await inner?.return?.()}catch(e){cleanupError=e}
|
|
177
239
|
done.resolve();try{await guard}catch(e){primaryError??=e}
|
|
178
240
|
aborted=Boolean(options.signal?.aborted||controller.signal.aborted||previousSignal?.aborted)
|
|
179
|
-
|
|
180
|
-
|
|
241
|
+
// Restore exactly what this call wrote, and nothing it did not: a request
|
|
242
|
+
// the Host never touched keeps its own shape, sealed or frozen alike.
|
|
243
|
+
if(changedSignal){if(signalAccess.descriptor)Object.defineProperty(options,'signal',signalAccess.descriptor);else delete options.signal}
|
|
244
|
+
if(changedOutput){if(tokenAccess.descriptor)Object.defineProperty(options,'maxTokens',tokenAccess.descriptor);else delete options.maxTokens}
|
|
181
245
|
record.finishedAt=now();record.usage=observed.usage;record.usageComplete=terminal&&observed.complete;record.finishKind=finishKind
|
|
182
246
|
record.state=started&&(!terminal||cleanupError)?'unknown':'settled'
|
|
183
247
|
record.outcome=!started?'cancelled':!terminal||cleanupError?'unknown':aborted||finishKind==='aborted'?'cancelled':finishKind==='error'?'failed':'succeeded'
|
|
184
248
|
try{
|
|
185
249
|
if(managed){if(lease)await store.recordModelReceipt(record)}
|
|
186
250
|
else if(accepted?.accepted){
|
|
187
|
-
const run=build();await createBudgetReservations({state:callStore(record,config,estimate)}).finishExecution({reservationId:accepted.reservationId,ownerToken:accepted.ownerToken,run})
|
|
251
|
+
const run=build();await createBudgetReservations({state:callStore(record,config,estimate,assertFunding)}).finishExecution({reservationId:accepted.reservationId,ownerToken:accepted.ownerToken,run})
|
|
188
252
|
}
|
|
189
253
|
}finally{
|
|
190
254
|
if(lease)authority.release(lease.token);active.delete(options);controllerSet.delete(controller)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// The same-generation seam for the Bot's auxiliary model calls.
|
|
2
|
+
//
|
|
3
|
+
// DS Bot makes three model requests that are not a conversation step: the handoff note
|
|
4
|
+
// (`writeNote`), the in-place checkpoint (`writeCheckpoint`) and the context steward's
|
|
5
|
+
// decision (`askSteward`). All three are bare `ctx.llm.stream({purpose:'compaction'})`
|
|
6
|
+
// calls, so the SDK resolves the route's current registration at dispatch — the same
|
|
7
|
+
// mispricing window `model-prepared-generation.mjs` closes for the independent path and
|
|
8
|
+
// `model-loop-frame.mjs` reads around for the integrated conversation. An auxiliary call
|
|
9
|
+
// cannot borrow either: the session log's `request/header` fold is the conversation's own
|
|
10
|
+
// metadata (experiment E3 in
|
|
11
|
+
// tmp/integration/integrated-generation-study-1/experiment.mjs: an aux call read
|
|
12
|
+
// generation A and dispatched generation B), and an aux request carries no loop marker.
|
|
13
|
+
//
|
|
14
|
+
// This module puts the auxiliary calls on the one public seam that binds metadata to its
|
|
15
|
+
// own dispatch: `LlmRuntime.prepareCall(config, signal, configure)` (dsh-llm
|
|
16
|
+
// lib/index.js:2220-2261). It composes the product's own prepared dispatcher, so the
|
|
17
|
+
// auxiliary request is prepared once, its exact capacities are recorded in the Host-owned
|
|
18
|
+
// frame store the accounting already reads, and its one-shot stream dispatches that same
|
|
19
|
+
// capture through the ordinary `llm/stream` waterfall exactly once — no listener
|
|
20
|
+
// re-prepares, no second waterfall, no second receipt.
|
|
21
|
+
//
|
|
22
|
+
// Three rules hold the trust boundary:
|
|
23
|
+
//
|
|
24
|
+
// 1. Identity is Host state, resolved before anything is prepared. `identityFor` is the
|
|
25
|
+
// parent's own `policy.modelIdentity`, which reads the principal directory for the
|
|
26
|
+
// request's Session. A request that names a Bot, a subject, a grant or a funding
|
|
27
|
+
// allowance of its own is refused: those are never caller inputs.
|
|
28
|
+
// 2. The Host's own redactor runs before preparation. The frozen ds-bot secret listener
|
|
29
|
+
// (vendor src/host/secrets.js:505-514) rebuilds a request and re-enters
|
|
30
|
+
// `ctx.llm.stream` when it still finds a value; a prepared generation cannot survive
|
|
31
|
+
// that rebuild, and the product listener refuses it
|
|
32
|
+
// (`MODEL_PREPARED_REQUEST_REWRITTEN`) rather than price one generation and stream
|
|
33
|
+
// another. Redacting first is what makes the rewrite unnecessary, exactly as the
|
|
34
|
+
// independent path does.
|
|
35
|
+
// 3. Nothing here holds money. The allowance read through `fundingFor` is only a capacity
|
|
36
|
+
// to clamp the dispatched output bound against, before the SDK freezes the config; the
|
|
37
|
+
// one accounting remains the sole authority over reservations, receipts and cost, and
|
|
38
|
+
// a missing or throwing read is treated as unpriced so it refuses in its own coded way.
|
|
39
|
+
//
|
|
40
|
+
// The steward's `reasoningEffort:'off'` is decided from the generation being prepared. The
|
|
41
|
+
// prepared handle deliberately does not expose the model's reasoning metadata (dsh-llm
|
|
42
|
+
// lib/index.js:2241-2248), and `resolveModelInfo` reads the current registration — another
|
|
43
|
+
// generation — so the only public place that knows is the `configure` callback the SDK
|
|
44
|
+
// itself calls with the captured `modelInfo`. `createPreparedModelDispatch` owns that
|
|
45
|
+
// callback and does not expose it, so this module supplies the runtime it prepares through:
|
|
46
|
+
// a Host-owned service whose `prepareCall` wraps the real one and adds the caller's control
|
|
47
|
+
// selection to the SDK's own callback. No SDK file, vendor file or global is touched, the
|
|
48
|
+
// wrapper is built per call (no shared state, no concurrency hazard), and an effort the
|
|
49
|
+
// prepared generation does not support is never sent.
|
|
50
|
+
|
|
51
|
+
import { createPreparedModelDispatch, redactPreparedModelRequest } from './model-prepared-generation.mjs'
|
|
52
|
+
|
|
53
|
+
const fail = (code, detail) => { throw Object.assign(new Error(code), { code, ...detail }) }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Request keys an auxiliary caller may never supply.
|
|
57
|
+
*
|
|
58
|
+
* Identity, authority and money are committed Host state. A request that carries any of
|
|
59
|
+
* them is refused rather than silently ignored: a caller that believes it named the Bot
|
|
60
|
+
* must not go on believing the call ran for that Bot.
|
|
61
|
+
*/
|
|
62
|
+
const UNTRUSTED_KEYS = Object.freeze([
|
|
63
|
+
'botId', 'subject', 'principal', 'grant', 'authority', 'identity', 'token', 'lease', 'binding',
|
|
64
|
+
'funding', 'allowance', 'maxOutputTokens', 'maxInputTokens', 'pricing', 'scopeId', 'projectId',
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The steward's control selection: `reasoningEffort:'off'` when — and only when — the
|
|
69
|
+
* generation being prepared declares that effort.
|
|
70
|
+
*
|
|
71
|
+
* @param info - the prepared generation's normalized model info, exactly as the SDK passes
|
|
72
|
+
* it to `configure` (dsh-llm lib/index.js:2228).
|
|
73
|
+
* @returns `{reasoningEffort:'off'}`, or `undefined` when the generation supports no `off`
|
|
74
|
+
* effort or declares no reasoning at all, in which case the adapter's own default stands
|
|
75
|
+
* — the behaviour the bare stream had.
|
|
76
|
+
*/
|
|
77
|
+
export function auxiliaryReasoningOff(info) {
|
|
78
|
+
const efforts = info?.reasoning?.efforts
|
|
79
|
+
if (!Array.isArray(efforts)) return undefined
|
|
80
|
+
return efforts.some(effort => effort && effort.id === 'off') ? { reasoningEffort: 'off' } : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wrap one runtime so its `prepareCall` adds the caller's control selection to the SDK's
|
|
85
|
+
* own `configure` callback.
|
|
86
|
+
*
|
|
87
|
+
* The wrapper is a Host-owned object whose prototype is the live service, so every other
|
|
88
|
+
* member of `LlmRuntime` is the real one and the real service stays `this` inside
|
|
89
|
+
* `prepareCall`. The decorated object is built per call and never escapes this module.
|
|
90
|
+
*/
|
|
91
|
+
function withConfiguredControls(runtime, controlsFor) {
|
|
92
|
+
if (typeof controlsFor !== 'function') return runtime
|
|
93
|
+
const decorated = Object.create(runtime)
|
|
94
|
+
Object.defineProperty(decorated, 'prepareCall', {
|
|
95
|
+
value: (config, signal, configure) => runtime.prepareCall(config, signal, (controls, info) => {
|
|
96
|
+
let selected
|
|
97
|
+
try { selected = controlsFor(info) } catch (error) {
|
|
98
|
+
fail('MODEL_AUXILIARY_CONTROLS_REJECTED', { reason: String(error?.code ?? error?.message ?? error) })
|
|
99
|
+
}
|
|
100
|
+
// Only the call-config fields matter to the SDK; anything else a control selection
|
|
101
|
+
// returns is dropped rather than forwarded into the dispatched request.
|
|
102
|
+
let next = controls
|
|
103
|
+
if (selected && typeof selected === 'object') {
|
|
104
|
+
next = { ...controls }
|
|
105
|
+
for (const [key, value] of Object.entries(selected)) if (value !== undefined) next[key] = value
|
|
106
|
+
}
|
|
107
|
+
// The dispatcher's own configure still decides the output bound and records the
|
|
108
|
+
// capacities the accounting admits on; this wrapper only adds the caller's selection.
|
|
109
|
+
return configure(next, info)
|
|
110
|
+
}),
|
|
111
|
+
writable: true, configurable: true, enumerable: false,
|
|
112
|
+
})
|
|
113
|
+
return decorated
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The prepared auxiliary stream for the Bot's note, checkpoint and steward calls.
|
|
118
|
+
*
|
|
119
|
+
* @param llm - the live `llm` service, or a function returning it. A Host whose runtime
|
|
120
|
+
* exposes no `prepareCall` is refused per call (`MODEL_PREPARED_UNAVAILABLE`), never
|
|
121
|
+
* downgraded to the unprepared stream that re-resolves the model at dispatch.
|
|
122
|
+
* @param frames - the shared {@link createModelFrameStore} the one accounting reads. The
|
|
123
|
+
* same store the independent path and the composition use, so an auxiliary frame is
|
|
124
|
+
* admitted by the same authority and for exactly as long as its one dispatch.
|
|
125
|
+
* @param fundingFor - `({botId, provider, model}) => funding | null`, the Host's committed
|
|
126
|
+
* funding read. Only its priced output allowance is used, and only as a capacity to clamp
|
|
127
|
+
* against; a throw or a null is unpriced, and the accounting still refuses in its own way.
|
|
128
|
+
* @param identityFor - `(request) => {botId, subject, ...} | null`, the parent's trusted
|
|
129
|
+
* Host identity read (`policy.modelIdentity`). It is the only source of the Bot and the
|
|
130
|
+
* subject; a request that names its own is refused.
|
|
131
|
+
* @param redact - the Host's public string redactor, applied to `messages` and `system`
|
|
132
|
+
* before preparation so the frozen secret listener has nothing to rebuild.
|
|
133
|
+
* @param carryUsageTag - optional `(from, to) => void`, the runtime's own usage-tag carry,
|
|
134
|
+
* so a redaction rebuild keeps the DS Bot usage provenance the tag records.
|
|
135
|
+
* @returns a callable `stream(request, {controls})` — also reachable as `.stream` — that is
|
|
136
|
+
* an async iterable of chunks, plus `reasoningOff` for the steward's decision.
|
|
137
|
+
*/
|
|
138
|
+
export function createAuxiliaryModelStream({
|
|
139
|
+
llm = null, frames = null, fundingFor = null, identityFor = null, redact = null, carryUsageTag = null,
|
|
140
|
+
} = {}) {
|
|
141
|
+
if (!frames || typeof frames.set !== 'function' || typeof frames.read !== 'function') fail('MODEL_FRAME_STORE_REQUIRED')
|
|
142
|
+
if (typeof identityFor !== 'function') fail('MODEL_AUXILIARY_IDENTITY_REQUIRED')
|
|
143
|
+
if (typeof redact !== 'function') fail('MODEL_AUXILIARY_REDACTION_REQUIRED')
|
|
144
|
+
const service = () => (typeof llm === 'function' ? llm() : llm)
|
|
145
|
+
|
|
146
|
+
async function* stream(request, context = {}) {
|
|
147
|
+
if (!request || typeof request !== 'object' || Array.isArray(request)) fail('MODEL_AUXILIARY_REQUEST_REQUIRED')
|
|
148
|
+
for (const key of UNTRUSTED_KEYS) if (request[key] !== undefined) {
|
|
149
|
+
fail('MODEL_AUXILIARY_IDENTITY_UNTRUSTED', { reason: key })
|
|
150
|
+
}
|
|
151
|
+
if (typeof request.provider !== 'string' || request.provider === '' || typeof request.model !== 'string' || request.model === '') {
|
|
152
|
+
fail('MODEL_AUXILIARY_ROUTE_REQUIRED')
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(request.messages)) fail('MODEL_AUXILIARY_MESSAGES_REQUIRED')
|
|
155
|
+
|
|
156
|
+
// 1. Trusted identity, before anything is prepared: whose call this is, decided by the
|
|
157
|
+
// Host's own directory for this request's Session — never by the request.
|
|
158
|
+
let identity = null
|
|
159
|
+
try { identity = await identityFor(request) } catch (error) {
|
|
160
|
+
fail('MODEL_AUXILIARY_IDENTITY_REQUIRED', { reason: String(error?.code ?? error?.message ?? error) })
|
|
161
|
+
}
|
|
162
|
+
const botId = identity?.botId
|
|
163
|
+
if (typeof botId !== 'string' || botId === '') fail('MODEL_AUXILIARY_IDENTITY_REQUIRED')
|
|
164
|
+
|
|
165
|
+
// 2. Host redaction, before the generation is bound. The redactor is the Host's own; a
|
|
166
|
+
// value it finds is replaced here, so the frozen secret listener passes this request
|
|
167
|
+
// straight through instead of rebuilding it and losing the prepared generation.
|
|
168
|
+
const redacted = redactPreparedModelRequest(request, redact)
|
|
169
|
+
if (typeof carryUsageTag === 'function') carryUsageTag(request.messages, redacted.messages)
|
|
170
|
+
|
|
171
|
+
// 3. One preparation, one dispatch, one frame. The dispatcher is built per call so the
|
|
172
|
+
// control selection above belongs to this request alone, and the shared frame store
|
|
173
|
+
// is what the accounting reads.
|
|
174
|
+
const runtime = service()
|
|
175
|
+
if (!runtime || typeof runtime.prepareCall !== 'function') fail('MODEL_PREPARED_UNAVAILABLE')
|
|
176
|
+
const dispatch = createPreparedModelDispatch({
|
|
177
|
+
llm: () => withConfiguredControls(runtime, typeof context.controls === 'function' ? context.controls : null),
|
|
178
|
+
frames,
|
|
179
|
+
...(typeof fundingFor === 'function' ? { fundingFor } : {}),
|
|
180
|
+
})
|
|
181
|
+
yield* dispatch.dispatch(redacted, { botId })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
stream.stream = stream
|
|
185
|
+
stream.reasoningOff = auxiliaryReasoningOff
|
|
186
|
+
return Object.freeze(stream)
|
|
187
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// The same-generation frame for the integrated AgentLoop path, read from the session log.
|
|
2
|
+
//
|
|
3
|
+
// The installed SDK's `LlmRuntime.prepareCall` is the only public seam that binds one
|
|
4
|
+
// adapter registration's metadata to its own dispatch (dsh-llm lib/index.js 2220-2261:
|
|
5
|
+
// capture the registration at 2224, one-shot dispatch at 2249-2258). The integrated
|
|
6
|
+
// AgentLoop already uses it (dsh-agent-loop lib/index.js:1201) and, BEFORE dispatch,
|
|
7
|
+
// synchronously logs that exact generation into its Session: `request/header` with the
|
|
8
|
+
// resolved config plus the `adapterDefaults` flags (lib/index.js:1226-1240) and
|
|
9
|
+
// `request/context` with the context window (lib/index.js:1262-1271). The fold of those
|
|
10
|
+
// events (`Session.requestHeader()` / `Session.requestContext()`, dsh-session
|
|
11
|
+
// lib/index.js:1612/1627) therefore carries the generation the loop's frozen marked
|
|
12
|
+
// request will dispatch, even when the current registration was replaced after the
|
|
13
|
+
// loop's prepareCall — proven causally by experiment E1 in
|
|
14
|
+
// tmp/integration/integrated-generation-study-1/experiment.mjs (dispatch A, current
|
|
15
|
+
// registration read B, session-log fold A).
|
|
16
|
+
//
|
|
17
|
+
// The fold is the CONVERSATION's latest state, not a per-call receipt: it is not, by
|
|
18
|
+
// itself, proof that it describes this one call. This module therefore derives a frame
|
|
19
|
+
// only for a request that carries every public anchor the AgentLoop leaves on it, and
|
|
20
|
+
// answers with a verdict the caller must act on rather than a bare frame-or-null:
|
|
21
|
+
//
|
|
22
|
+
// frame the fold describes this exact request; price and admit on it.
|
|
23
|
+
// aux not a conversation step (no loop marker, or an aux purpose). The
|
|
24
|
+
// caller keeps its existing path — an ordinary request, or a
|
|
25
|
+
// caller-owned prepared call that carries its own frame.
|
|
26
|
+
// unsupportedHost the Host could not answer at all (no session lookup wired, a
|
|
27
|
+
// sessions service that is not a SessionStore, a throwing read).
|
|
28
|
+
// The product refuses API funding; subscription callers may retain
|
|
29
|
+
// their unpriced path. This verdict supplies no captured frame.
|
|
30
|
+
// missing a loop step whose record is absent or unusable (no fold, no live
|
|
31
|
+
// session, no materialized output bound). The caller refuses the
|
|
32
|
+
// funded call rather than price another generation.
|
|
33
|
+
// invalid a loop step whose record contradicts the request (route drift,
|
|
34
|
+
// control drift, session identity mismatch). The caller refuses.
|
|
35
|
+
//
|
|
36
|
+
// SDK-free by construction: the loop-marker predicate and the session lookup are
|
|
37
|
+
// injected, so orchestration keeps its no-direct-SDK-import shape and a request cannot
|
|
38
|
+
// reach a conversation frame it did not earn from the SDK's own marker.
|
|
39
|
+
|
|
40
|
+
const positive = value => Number.isSafeInteger(value) && value > 0 ? value : null
|
|
41
|
+
const isId = value => typeof value === 'string' && value.length > 0 && value.length <= 512
|
|
42
|
+
|
|
43
|
+
/** What one resolution found. See the module comment for what the caller owes each. */
|
|
44
|
+
export const LOOP_FRAME_VERDICT = Object.freeze({
|
|
45
|
+
FRAME: 'frame', AUX: 'aux', UNSUPPORTED_HOST: 'unsupportedHost', MISSING: 'missing', INVALID: 'invalid',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
/** The reason attached to a non-frame verdict; stable, loggable, never a message. */
|
|
49
|
+
export const LOOP_FRAME_REASON = Object.freeze({
|
|
50
|
+
HOST_LOOKUP_UNWIRED: 'host-lookup-unwired',
|
|
51
|
+
HOST_LOOKUP_FAILED: 'host-lookup-failed',
|
|
52
|
+
HOST_SESSION_SERVICE_UNUSABLE: 'host-session-service-unusable',
|
|
53
|
+
HOST_SESSION_NOT_LIVE: 'host-session-not-live',
|
|
54
|
+
HOST_SESSION_IDENTITY_MISMATCH: 'host-session-identity-mismatch',
|
|
55
|
+
HOST_FOLD_UNREADABLE: 'host-fold-unreadable',
|
|
56
|
+
LOOP_SESSION_ID_MISSING: 'loop-session-id-missing',
|
|
57
|
+
LOOP_REQUEST_NOT_CANONICAL: 'loop-request-not-canonical',
|
|
58
|
+
LOOP_HEADER_FOLD_ABSENT: 'loop-header-fold-absent',
|
|
59
|
+
LOOP_CONTEXT_FOLD_ABSENT: 'loop-context-fold-absent',
|
|
60
|
+
LOOP_ROUTE_DRIFT: 'loop-route-drift',
|
|
61
|
+
LOOP_CONTROL_DRIFT: 'loop-control-drift',
|
|
62
|
+
LOOP_CONTEXT_WINDOW_UNBOUND: 'loop-context-window-unbound',
|
|
63
|
+
LOOP_OUTPUT_BOUND_UNMATERIALIZED: 'loop-output-bound-unmaterialized',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const verdict = (kind, reason) => Object.freeze(reason === undefined ? { kind } : { kind, reason })
|
|
67
|
+
|
|
68
|
+
// The call-config fields the SDK compares for one call (`callConfigEquals`, dsh-llm
|
|
69
|
+
// lib/index.js:367-371). The loop request is spread from the logged header's config, so
|
|
70
|
+
// any disagreement means the fold in hand belongs to a different step of the
|
|
71
|
+
// conversation — a stale fold, not this dispatch.
|
|
72
|
+
const CONTROL_FIELDS = Object.freeze(['provider', 'model', 'reasoningEffort', 'temperature', 'maxTokens'])
|
|
73
|
+
const controlDrift = (config, request) => {
|
|
74
|
+
for (const field of CONTROL_FIELDS) if (config[field] !== request[field]) return LOOP_FRAME_REASON.LOOP_CONTROL_DRIFT
|
|
75
|
+
if (config.stop === undefined || request.stop === undefined) {
|
|
76
|
+
if (config.stop !== request.stop) return LOOP_FRAME_REASON.LOOP_CONTROL_DRIFT
|
|
77
|
+
} else if (config.stop.length !== request.stop.length || config.stop.some((item, index) => item !== request.stop[index])) {
|
|
78
|
+
return LOOP_FRAME_REASON.LOOP_CONTROL_DRIFT
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Derive a Host-owned same-generation frame for one integrated-loop request.
|
|
85
|
+
*
|
|
86
|
+
* @param sessionFor - `(sessionId) => session | null | undefined`; in the composed
|
|
87
|
+
* product this is `id => ctx.get('sessions')?.get?.(id)` — the Host SessionStore the
|
|
88
|
+
* AgentLoop itself appends through (dsh-session lib/index.js:1980). Absent, throwing
|
|
89
|
+
* or not a SessionStore is `unsupportedHost`, never an error and never a frame.
|
|
90
|
+
* @param isLoopRequest - the SDK's `isAgentLoopRequest` (re-exported through
|
|
91
|
+
* modules/bot-runtime), so a hand-built request carrying a conversation's `sessionId`
|
|
92
|
+
* cannot reach that conversation's frame: the marker is a process-private WeakSet the
|
|
93
|
+
* SDK alone can add to (dsh-llm lib/index.js:377-388), and it does not survive a copy.
|
|
94
|
+
* @returns `(request) => verdict`, where a `frame` verdict carries the frozen
|
|
95
|
+
* `{provider, model, bounds: {contextWindow, defaultMaxTokens}, request}` shape
|
|
96
|
+
* model-accounting.mjs:89-96 consumes.
|
|
97
|
+
*/
|
|
98
|
+
export function createSessionLoopFrameResolver({ sessionFor = null, isLoopRequest = null, allowUnbounded = null } = {}) {
|
|
99
|
+
const lookup = typeof sessionFor === 'function' ? sessionFor : null
|
|
100
|
+
const marked = typeof isLoopRequest === 'function' ? isLoopRequest : null
|
|
101
|
+
const aux = () => verdict(LOOP_FRAME_VERDICT.AUX)
|
|
102
|
+
const unsupported = reason => verdict(LOOP_FRAME_VERDICT.UNSUPPORTED_HOST, reason)
|
|
103
|
+
|
|
104
|
+
// Without the SDK's marker predicate nothing can be told apart, so the resolver is
|
|
105
|
+
// inert. A request shaped like a loop envelope is reported as unsupported — the gap
|
|
106
|
+
// is open — while everything else keeps the path it already had. The shape is a
|
|
107
|
+
// heuristic for reporting only; it grants nothing.
|
|
108
|
+
const loopShaped = request => request && typeof request === 'object' && isId(request.sessionId)
|
|
109
|
+
&& request.purpose === undefined && Object.isFrozen(request)
|
|
110
|
+
&& Array.isArray(request.messages) && Object.isFrozen(request.messages)
|
|
111
|
+
if (!marked) return request => (loopShaped(request) ? unsupported(LOOP_FRAME_REASON.HOST_LOOKUP_UNWIRED) : aux())
|
|
112
|
+
// The Host did not wire the session seam at all. A loop step is reported as
|
|
113
|
+
// unsupported — the gap is open — while an ordinary or aux request keeps its path.
|
|
114
|
+
if (!lookup) return request => (request && typeof request === 'object' && marked(request) && request.purpose === undefined
|
|
115
|
+
? unsupported(LOOP_FRAME_REASON.HOST_LOOKUP_UNWIRED)
|
|
116
|
+
: aux())
|
|
117
|
+
|
|
118
|
+
return request => {
|
|
119
|
+
if (!request || typeof request !== 'object' || !marked(request)) return aux()
|
|
120
|
+
// A marked request that names an aux purpose is not a conversation step: the
|
|
121
|
+
// conversation's frame must never price a compaction, a title or a note
|
|
122
|
+
// (experiment E3), and a caller-owned prepared aux binds its own dispatch anyway.
|
|
123
|
+
if (request.purpose !== undefined) return aux()
|
|
124
|
+
if (!isId(request.sessionId)) return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.LOOP_SESSION_ID_MISSING)
|
|
125
|
+
// The loop freezes its request envelope and the messages it derived
|
|
126
|
+
// (dsh-agent-loop lib/index.js:1274-1288). A request that is not that envelope is
|
|
127
|
+
// not the one the fold was logged for, whatever it claims.
|
|
128
|
+
if (!Object.isFrozen(request) || !Array.isArray(request.messages) || !Object.isFrozen(request.messages)) {
|
|
129
|
+
return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.LOOP_REQUEST_NOT_CANONICAL)
|
|
130
|
+
}
|
|
131
|
+
let session = null
|
|
132
|
+
try { session = lookup(request.sessionId) } catch { return unsupported(LOOP_FRAME_REASON.HOST_LOOKUP_FAILED) }
|
|
133
|
+
if (session === null || session === undefined) return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.HOST_SESSION_NOT_LIVE)
|
|
134
|
+
if (typeof session.requestContext !== 'function' || typeof session.requestHeader !== 'function') {
|
|
135
|
+
return unsupported(LOOP_FRAME_REASON.HOST_SESSION_SERVICE_UNUSABLE)
|
|
136
|
+
}
|
|
137
|
+
// The lookup must answer about the session the request names, not some other one.
|
|
138
|
+
if (session.id !== request.sessionId) return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.HOST_SESSION_IDENTITY_MISMATCH)
|
|
139
|
+
let context = null, header = null
|
|
140
|
+
try { context = session.requestContext(); header = session.requestHeader() } catch { return unsupported(LOOP_FRAME_REASON.HOST_FOLD_UNREADABLE) }
|
|
141
|
+
if (!context || typeof context !== 'object') return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.LOOP_CONTEXT_FOLD_ABSENT)
|
|
142
|
+
if (!header || typeof header !== 'object' || !header.config || typeof header.config !== 'object') {
|
|
143
|
+
return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.LOOP_HEADER_FOLD_ABSENT)
|
|
144
|
+
}
|
|
145
|
+
// The fold must describe this exact route on both surfaces. A conversation that
|
|
146
|
+
// moved to another route between the loop's log and this read has no frame for it,
|
|
147
|
+
// and pricing the old route's window against the new endpoint is the very mixing
|
|
148
|
+
// this module exists to prevent.
|
|
149
|
+
if (context.provider !== request.provider || context.model !== request.model
|
|
150
|
+
|| header.config.provider !== request.provider || header.config.model !== request.model) {
|
|
151
|
+
return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.LOOP_ROUTE_DRIFT)
|
|
152
|
+
}
|
|
153
|
+
const contextWindow = positive(context.contextWindow)
|
|
154
|
+
if (controlDrift(header.config, request) !== null) return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.LOOP_CONTROL_DRIFT)
|
|
155
|
+
// Only an explicitly Host-classified subscription route may preserve unknown
|
|
156
|
+
// capacities. It still carries the same canonical prepared route and controls;
|
|
157
|
+
// accounting never receives fabricated capacity or an API estimate from it.
|
|
158
|
+
const unbounded = typeof allowUnbounded === 'function' && allowUnbounded(request) === true
|
|
159
|
+
if (contextWindow === null && !unbounded) return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.LOOP_CONTEXT_WINDOW_UNBOUND)
|
|
160
|
+
// The dispatched output bound is already materialized into the frozen request by
|
|
161
|
+
// the SDK's own call-config resolution (dsh-llm lib/index.js:2183-2186), so the
|
|
162
|
+
// generation that streams is the one admitted. A loop step that carries no
|
|
163
|
+
// materialized bound (the NO_ADAPTER fallback at dsh-agent-loop lib/index.js:1084
|
|
164
|
+
// dispatches a bare `llm.stream`) has no same-generation frame: the caller refuses
|
|
165
|
+
// it instead of falling back to a read that can name the other generation.
|
|
166
|
+
const dispatched = positive(request.maxTokens)
|
|
167
|
+
if (dispatched === null && !unbounded) return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.LOOP_OUTPUT_BOUND_UNMATERIALIZED)
|
|
168
|
+
// `adapterDefaults.maxTokens === true` marks that the SDK materialized the adapter's
|
|
169
|
+
// default into the logged config (dsh-llm lib/index.js:2236-2239); that value is the
|
|
170
|
+
// output default this generation dispatches. A bound the caller declared needs no
|
|
171
|
+
// default: the request already carries it.
|
|
172
|
+
const defaultMaxTokens = header.adapterDefaults?.maxTokens === true ? dispatched : null
|
|
173
|
+
const frame = Object.freeze({
|
|
174
|
+
provider: request.provider, model: request.model,
|
|
175
|
+
bounds: Object.freeze({ contextWindow, defaultMaxTokens }), request,
|
|
176
|
+
})
|
|
177
|
+
return Object.freeze({ kind: LOOP_FRAME_VERDICT.FRAME, frame })
|
|
178
|
+
}
|
|
179
|
+
}
|