@citeark/agent 0.3.20
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/LICENSE +202 -0
- package/README.md +128 -0
- package/data/dataset-source-registry.v1.json +300 -0
- package/dist/arkgraph/boot.js +6 -0
- package/dist/arkgraph/index.html +1 -0
- package/dist/arkgraph/viewer.css +1 -0
- package/dist/arkgraph/viewer.en.css +1 -0
- package/dist/arkgraph/viewer.en.js +49 -0
- package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
- package/dist/arkgraph/viewer.js +49 -0
- package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
- package/docker/claude-code/Dockerfile +97 -0
- package/docker/claude-code/codex-pro-relay.mjs +466 -0
- package/docker/claude-code/runtime-contract-check.mjs +79 -0
- package/docs/arkgraph-reading.md +79 -0
- package/docs/configuration.md +100 -0
- package/docs/integration.md +92 -0
- package/docs/maturity-plan.md +27 -0
- package/docs/npm-release.md +44 -0
- package/docs/paper-reading.md +40 -0
- package/docs/research-plan-granularity.md +27 -0
- package/docs/terminal.md +49 -0
- package/examples/toy-evaluation/compile-task.json +27 -0
- package/examples/toy-evaluation/paper.md +5 -0
- package/examples/toy-evaluation/repository/README.md +9 -0
- package/examples/toy-evaluation/repository/checkpoint.json +4 -0
- package/examples/toy-evaluation/repository/evaluate.py +17 -0
- package/examples/toy-evaluation/task.json +81 -0
- package/package.json +59 -0
- package/prompts/compile-research.md +58 -0
- package/prompts/execute-contract.md +72 -0
- package/prompts/execute-workspace-simple.md +51 -0
- package/prompts/execute-workspace.md +34 -0
- package/prompts/prepare-reproduction.md +82 -0
- package/prompts/repair-research.md +45 -0
- package/protocol/CAP.md +129 -0
- package/protocol/LICENSE +12 -0
- package/protocol/MAPPINGS.md +72 -0
- package/protocol/README.md +38 -0
- package/protocol/conformance-v2.0-alpha.1.json +36 -0
- package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
- package/protocol/examples/arkgraph/fixtures.mjs +49 -0
- package/protocol/examples/arkgraph/paper-free.json +291 -0
- package/protocol/examples/arkgraph/partial-failure.json +344 -0
- package/protocol/examples/arkgraph/training-evaluation.json +443 -0
- package/protocol/profiles/agent-trace.md +16 -0
- package/protocol/profiles/computational-run.md +16 -0
- package/protocol/profiles/core.md +15 -0
- package/protocol/profiles/public-bundle.md +18 -0
- package/protocol/profiles/reproduction.md +29 -0
- package/protocol/profiles/research-compilation.md +44 -0
- package/protocol/profiles/research-plan.md +39 -0
- package/protocol/profiles/restricted-evidence.md +15 -0
- package/runtime/bootstrap-autodl-runtime.sh +314 -0
- package/runtime/create-runtime-venv.sh +41 -0
- package/runtime/install-local-cpu-runtime.sh +23 -0
- package/runtime/install-scientific-runtime.sh +153 -0
- package/runtime/mineru/parse.py +62 -0
- package/runtime/mineru/requirements.txt +4 -0
- package/runtime/requirements-baseline.txt +38 -0
- package/schemas/cap/v2/activity.schema.json +47 -0
- package/schemas/cap/v2/agent.schema.json +32 -0
- package/schemas/cap/v2/assertion.schema.json +110 -0
- package/schemas/cap/v2/descriptor.schema.json +243 -0
- package/schemas/cap/v2/entity.schema.json +64 -0
- package/schemas/cap/v2/manifest.schema.json +67 -0
- package/schemas/cap/v2/relation.schema.json +82 -0
- package/schemas/compute-catalog.schema.json +63 -0
- package/schemas/compute-decision.schema.json +27 -0
- package/schemas/execution-contract.schema.json +1024 -0
- package/schemas/research-card.schema.json +30 -0
- package/schemas/research-inventory-draft.schema.json +366 -0
- package/schemas/research.schema.json +1044 -0
- package/schemas/result.schema.json +173 -0
- package/schemas/verification-policy.schema.json +47 -0
- package/schemas/verified-conclusion.schema.json +58 -0
- package/schemas/workspace-summary.schema.json +24 -0
- package/scripts/build-arkgraph-view.mjs +12 -0
- package/scripts/check-execution-feasibility.mjs +24 -0
- package/scripts/check-syntax.mjs +15 -0
- package/scripts/deterministic-asset-preparation.py +438 -0
- package/scripts/package-cap.mjs +23 -0
- package/scripts/package-local-agent.mjs +23 -0
- package/scripts/preview-arkgraph.mjs +25 -0
- package/scripts/replay-research-compiler-candidate.mjs +134 -0
- package/scripts/review-compiler-sources.mjs +44 -0
- package/scripts/run-asset-preparation.sh +17 -0
- package/scripts/run-research-plan.mjs +98 -0
- package/scripts/validate-asset-preparation.py +290 -0
- package/scripts/verify-local-runtime.mjs +57 -0
- package/scripts/verify-npm-package.mjs +57 -0
- package/src/adapters/paper2agent.mjs +107 -0
- package/src/assets/cache.mjs +159 -0
- package/src/assets/compute.mjs +98 -0
- package/src/assets/executor.mjs +145 -0
- package/src/assets/lifecycle.mjs +213 -0
- package/src/assets/manifest.mjs +242 -0
- package/src/assets/opportunistic-preparation.mjs +81 -0
- package/src/assets/plan.mjs +411 -0
- package/src/assets/prompts.mjs +29 -0
- package/src/assets/public-asset-probe.mjs +525 -0
- package/src/assets/qualification.mjs +119 -0
- package/src/assets/readiness.mjs +130 -0
- package/src/assets/reproduction-admission.mjs +355 -0
- package/src/assets/requirements.mjs +152 -0
- package/src/assets/source-grounding.mjs +341 -0
- package/src/assets/source-policy.mjs +118 -0
- package/src/autodl/client.mjs +260 -0
- package/src/autodl/ssh.mjs +380 -0
- package/src/autodl/tools.mjs +129 -0
- package/src/cap/redaction.mjs +38 -0
- package/src/cap/v2/archive.mjs +152 -0
- package/src/cap/v2/attestation.mjs +204 -0
- package/src/cap/v2/canonical-json.mjs +114 -0
- package/src/cap/v2/compilation-artifact.mjs +240 -0
- package/src/cap/v2/core.mjs +282 -0
- package/src/cap/v2/measurement-assessment-records.mjs +23 -0
- package/src/cap/v2/pipeline-artifact.mjs +922 -0
- package/src/cap/v2/read.mjs +41 -0
- package/src/cap/v2/reassessment-artifact.mjs +383 -0
- package/src/cap/v2/research-artifact.mjs +231 -0
- package/src/cap/v2/research-map-records.mjs +46 -0
- package/src/cap/v2/research-object-records.mjs +163 -0
- package/src/cap/v2/research-records.mjs +187 -0
- package/src/cap/v2/verify.mjs +642 -0
- package/src/cli.mjs +1146 -0
- package/src/compute/autodl-pro-compiler.mjs +347 -0
- package/src/compute/autodl-pro-executor.mjs +459 -0
- package/src/compute/autodl-pro-job.mjs +843 -0
- package/src/compute/autodl-pro-network.mjs +295 -0
- package/src/compute/autodl-pro-remote.mjs +810 -0
- package/src/compute/autodl-pro-staging.mjs +117 -0
- package/src/compute/campaign.mjs +110 -0
- package/src/compute/catalog.mjs +123 -0
- package/src/compute/checkpoint-protocol.mjs +154 -0
- package/src/compute/codex-account-lock.mjs +111 -0
- package/src/compute/codex-account-session.mjs +107 -0
- package/src/compute/compiler-profile.mjs +38 -0
- package/src/compute/compiler-router.mjs +23 -0
- package/src/compute/coordinator-recovery.mjs +210 -0
- package/src/compute/executor-router.mjs +29 -0
- package/src/compute/gcp-batch-compiler.mjs +685 -0
- package/src/compute/gcp-batch-executor.mjs +1215 -0
- package/src/compute/gcp-batch-failure.mjs +92 -0
- package/src/compute/gcp-batch-job.mjs +527 -0
- package/src/compute/gcp-batch-lifecycle.mjs +81 -0
- package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
- package/src/compute/local-codex-compiler.mjs +52 -0
- package/src/compute/measurement-hardware.mjs +128 -0
- package/src/compute/remote-attempt.mjs +226 -0
- package/src/compute/requirements.mjs +124 -0
- package/src/compute/research-phases.mjs +48 -0
- package/src/compute/scheduler.mjs +452 -0
- package/src/compute/shared-workloads.mjs +26 -0
- package/src/compute/stage-archive.mjs +79 -0
- package/src/contracts/campaign-contract.mjs +52 -0
- package/src/contracts/execution-contract.mjs +819 -0
- package/src/contracts/execution-mode.mjs +19 -0
- package/src/contracts/execution-timeouts.mjs +45 -0
- package/src/contracts/execution-workload.mjs +68 -0
- package/src/contracts/preflight-schema.mjs +25 -0
- package/src/contracts/public-contract.mjs +63 -0
- package/src/contracts/subject-tags.mjs +31 -0
- package/src/dashboard/data.mjs +898 -0
- package/src/dashboard/server.mjs +79 -0
- package/src/dashboard/static/dashboard.css +366 -0
- package/src/dashboard/static/dashboard.js +560 -0
- package/src/dashboard/static/index.html +85 -0
- package/src/deployment/community-policy.mjs +9 -0
- package/src/deployment/environment.mjs +112 -0
- package/src/deployment/guided.mjs +98 -0
- package/src/deployment/handoff.mjs +102 -0
- package/src/deployment/local-contract.mjs +31 -0
- package/src/deployment/local.mjs +100 -0
- package/src/deployment/prepare.mjs +46 -0
- package/src/deployment/recipe.mjs +108 -0
- package/src/deployment/supplement.mjs +51 -0
- package/src/deployment/terminal.mjs +43 -0
- package/src/diagnosis/renderer.mjs +75 -0
- package/src/diagnosis/target-failure.mjs +46 -0
- package/src/evidence/parser-registry.mjs +54 -0
- package/src/evidence/parsers/fasttext-classification.mjs +82 -0
- package/src/evidence/parsers/json-scalar.mjs +96 -0
- package/src/evidence/parsers/simcse-senteval.mjs +104 -0
- package/src/evidence/parsers/starspace-classification.mjs +78 -0
- package/src/evidence/registry.mjs +147 -0
- package/src/execution/runner-audit.mjs +473 -0
- package/src/gcp/auth.mjs +106 -0
- package/src/gcp/batch-client.mjs +120 -0
- package/src/gcp/resource-discovery.mjs +177 -0
- package/src/gcp/rest.mjs +82 -0
- package/src/gcp/secret-manager.mjs +34 -0
- package/src/gcp/signed-url.mjs +133 -0
- package/src/gcp/storage.mjs +220 -0
- package/src/graph/command.mjs +41 -0
- package/src/graph/execution.mjs +97 -0
- package/src/graph/model.mjs +37 -0
- package/src/graph/presentation.mjs +110 -0
- package/src/graph/query.mjs +159 -0
- package/src/graph/research-relations.mjs +69 -0
- package/src/graph/source-page.mjs +12 -0
- package/src/graph/source-preview.mjs +34 -0
- package/src/graph/validate.mjs +76 -0
- package/src/job.mjs +496 -0
- package/src/network/autodl-routing-proxy.mjs +462 -0
- package/src/network/egress-proxy.mjs +158 -0
- package/src/observability/event-contract.mjs +230 -0
- package/src/observability/pipeline-monitor.mjs +166 -0
- package/src/pipeline/orchestrator.mjs +1281 -0
- package/src/pipeline/recovery-error.mjs +11 -0
- package/src/pipeline/replay.mjs +304 -0
- package/src/pipeline/shared-execution.mjs +115 -0
- package/src/pipeline/stage-checkpoint.mjs +86 -0
- package/src/pipeline/stage-recovery.mjs +101 -0
- package/src/pipeline/targets.mjs +110 -0
- package/src/process.mjs +143 -0
- package/src/protocol.mjs +312 -0
- package/src/provider/codex-account.mjs +44 -0
- package/src/provider/codex-completion.mjs +49 -0
- package/src/provider/completion.mjs +292 -0
- package/src/provider/model-client.mjs +44 -0
- package/src/provider/model-route.mjs +29 -0
- package/src/provider/openrouter-readiness.mjs +189 -0
- package/src/provider/reader-bridge.mjs +35 -0
- package/src/provider/relay.mjs +263 -0
- package/src/provider/runtime-auth.mjs +40 -0
- package/src/public/cap.d.mts +90 -0
- package/src/public/cap.mjs +12 -0
- package/src/public/contracts.d.mts +2 -0
- package/src/public/host.mjs +171 -0
- package/src/public/operations.d.mts +11 -0
- package/src/public/presentation.d.mts +4 -0
- package/src/records/views.mjs +26 -0
- package/src/remote/command.mjs +178 -0
- package/src/remote/ssh.mjs +59 -0
- package/src/repository-origin.mjs +81 -0
- package/src/reproduction/evidence-feedback.mjs +96 -0
- package/src/reproduction/incomplete-initialization.mjs +25 -0
- package/src/reproduction/lifecycle.mjs +253 -0
- package/src/reproduction/plan.mjs +132 -0
- package/src/reproduction/prompts.mjs +70 -0
- package/src/reproduction/runner.mjs +188 -0
- package/src/reproduction/summary.mjs +130 -0
- package/src/reproduction/workspace-mode.mjs +7 -0
- package/src/research/automatic-admission.mjs +156 -0
- package/src/research/compiler-coverage.mjs +85 -0
- package/src/research/compiler-failure.mjs +24 -0
- package/src/research/compiler-normalization-guards.mjs +112 -0
- package/src/research/compiler-repair.mjs +3 -0
- package/src/research/compiler.mjs +853 -0
- package/src/research/continuation-selection.mjs +26 -0
- package/src/research/execution-graph-context.mjs +43 -0
- package/src/research/experiment-importance.mjs +15 -0
- package/src/research/inventory-handoff.mjs +104 -0
- package/src/research/inventory-revisions.mjs +32 -0
- package/src/research/mineru-local.mjs +73 -0
- package/src/research/paper-command.mjs +19 -0
- package/src/research/paper-markdown.mjs +180 -0
- package/src/research/paper-source-map.mjs +69 -0
- package/src/research/planning-policy.mjs +88 -0
- package/src/research/reference-materials.mjs +11 -0
- package/src/research/reproduction-scope.mjs +30 -0
- package/src/research/research-map.mjs +94 -0
- package/src/research/research-objects.mjs +88 -0
- package/src/research/source-discovery.mjs +646 -0
- package/src/research/source-observations.mjs +75 -0
- package/src/research/source-review-cli-mcp.mjs +26 -0
- package/src/research/source-review-input.mjs +209 -0
- package/src/research/source-review-local-codex.mjs +36 -0
- package/src/research/source-review-model.mjs +70 -0
- package/src/research/source-review.mjs +173 -0
- package/src/research/structure.mjs +3163 -0
- package/src/research-card/renderer.mjs +277 -0
- package/src/research-card/verified-conclusion.mjs +143 -0
- package/src/results/output-registry.mjs +183 -0
- package/src/runtime/claude-code.mjs +52 -0
- package/src/runtime/codex-capacity-retry.mjs +87 -0
- package/src/runtime/codex.mjs +64 -0
- package/src/runtime/config.mjs +157 -0
- package/src/runtime/final-output.mjs +40 -0
- package/src/runtime/index.mjs +21 -0
- package/src/runtime/local-codex.mjs +74 -0
- package/src/runtime/opencode.mjs +95 -0
- package/src/runtime/prompt.mjs +13 -0
- package/src/sandbox/docker.mjs +363 -0
- package/src/settings/command.mjs +297 -0
- package/src/settings/store.mjs +119 -0
- package/src/telemetry/pricing.mjs +68 -0
- package/src/telemetry/usage.mjs +265 -0
- package/src/terminal/events.mjs +97 -0
- package/src/terminal/input.mjs +40 -0
- package/src/terminal/plain.mjs +40 -0
- package/src/terminal/remote-stream.mjs +22 -0
- package/src/terminal/screen.mjs +214 -0
- package/src/terminal/transcript.mjs +69 -0
- package/src/util.mjs +107 -0
- package/src/verification/ai-assessor.mjs +534 -0
- package/src/verification/claim-evaluator.mjs +242 -0
- package/src/verification/evidence-context.mjs +165 -0
- package/src/verification/evidence-reader.mjs +95 -0
- package/src/verification/integrity.mjs +570 -0
- package/src/verification/tolerance.mjs +32 -0
- package/src/workloads/cpu-research-preparation.mjs +56 -0
- package/src/workloads/definition.mjs +74 -0
- package/src/workloads/phase-aware-reproduction.mjs +46 -0
- package/src/workloads/reproduction.mjs +85 -0
- package/src/workspace/command.mjs +242 -0
- package/src/workspace/control.mjs +49 -0
- package/src/workspace/entry.mjs +28 -0
- package/src/workspace/input.mjs +93 -0
- package/src/workspace/interactive.mjs +94 -0
- package/src/workspace/jobs.mjs +418 -0
- package/src/workspace/session.mjs +97 -0
- package/src/workspace/worker.mjs +137 -0
- package/ui/arkgraph/ambient-motion.mjs +10 -0
- package/ui/arkgraph/app.jsx +153 -0
- package/ui/arkgraph/boot.js +6 -0
- package/ui/arkgraph/camera-motion.mjs +20 -0
- package/ui/arkgraph/context-reveal.mjs +39 -0
- package/ui/arkgraph/details.css +3 -0
- package/ui/arkgraph/entry.jsx +28 -0
- package/ui/arkgraph/experiment-curves.mjs +17 -0
- package/ui/arkgraph/experiment-selection.mjs +15 -0
- package/ui/arkgraph/experiment-style.css +26 -0
- package/ui/arkgraph/experiment-ui.jsx +32 -0
- package/ui/arkgraph/frame.html +1 -0
- package/ui/arkgraph/graph-gestures.mjs +62 -0
- package/ui/arkgraph/label-layout.mjs +57 -0
- package/ui/arkgraph/locales/en.json +229 -0
- package/ui/arkgraph/locales/source-types.json +15 -0
- package/ui/arkgraph/localization-build.mjs +27 -0
- package/ui/arkgraph/material-build.mjs +23 -0
- package/ui/arkgraph/material-colors.mjs +39 -0
- package/ui/arkgraph/material-style.css +15 -0
- package/ui/arkgraph/open-graph.jsx +326 -0
- package/ui/arkgraph/outline.jsx +49 -0
- package/ui/arkgraph/package-lock.json +888 -0
- package/ui/arkgraph/package.json +17 -0
- package/ui/arkgraph/reading-layout.mjs +130 -0
- package/ui/arkgraph/reading-presentation.mjs +73 -0
- package/ui/arkgraph/record-detail.css +51 -0
- package/ui/arkgraph/record-details.jsx +29 -0
- package/ui/arkgraph/research-types.mjs +31 -0
- package/ui/arkgraph/selection-mark.jsx +6 -0
- package/ui/arkgraph/soft-spine.mjs +26 -0
- package/ui/arkgraph/steering-style.css +187 -0
- package/ui/arkgraph/style.css +272 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Bundled license information:
|
|
2
|
+
|
|
3
|
+
react/cjs/react.production.js:
|
|
4
|
+
/**
|
|
5
|
+
* @license React
|
|
6
|
+
* react.production.js
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
9
|
+
*
|
|
10
|
+
* This source code is licensed under the MIT license found in the
|
|
11
|
+
* LICENSE file in the root directory of this source tree.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
scheduler/cjs/scheduler.production.js:
|
|
15
|
+
/**
|
|
16
|
+
* @license React
|
|
17
|
+
* scheduler.production.js
|
|
18
|
+
*
|
|
19
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
20
|
+
*
|
|
21
|
+
* This source code is licensed under the MIT license found in the
|
|
22
|
+
* LICENSE file in the root directory of this source tree.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
react-dom/cjs/react-dom.production.js:
|
|
26
|
+
/**
|
|
27
|
+
* @license React
|
|
28
|
+
* react-dom.production.js
|
|
29
|
+
*
|
|
30
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
31
|
+
*
|
|
32
|
+
* This source code is licensed under the MIT license found in the
|
|
33
|
+
* LICENSE file in the root directory of this source tree.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
react-dom/cjs/react-dom-client.production.js:
|
|
37
|
+
/**
|
|
38
|
+
* @license React
|
|
39
|
+
* react-dom-client.production.js
|
|
40
|
+
*
|
|
41
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
42
|
+
*
|
|
43
|
+
* This source code is licensed under the MIT license found in the
|
|
44
|
+
* LICENSE file in the root directory of this source tree.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
lodash-es/lodash.js:
|
|
48
|
+
/**
|
|
49
|
+
* @license
|
|
50
|
+
* Lodash (Custom Build) <https://lodash.com/>
|
|
51
|
+
* Build: `lodash modularize exports="es" --repo lodash/lodash#4.18.1 -o ./`
|
|
52
|
+
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
|
|
53
|
+
* Released under MIT license <https://lodash.com/license>
|
|
54
|
+
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
|
|
55
|
+
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
|
56
|
+
*/
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
ARG BASE_IMAGE=node:22-bookworm
|
|
2
|
+
ARG UV_VERSION=0.12.7
|
|
3
|
+
FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv-runtime
|
|
4
|
+
FROM rust:1.85.1-bookworm AS rust-runtime
|
|
5
|
+
FROM ${BASE_IMAGE}
|
|
6
|
+
|
|
7
|
+
ARG CLAUDE_CODE_VERSION=2.1.227
|
|
8
|
+
ARG CODEX_VERSION=0.159.1
|
|
9
|
+
ARG OPENCODE_VERSION=1.18.14
|
|
10
|
+
ARG INSTALL_SYSTEM_DEPENDENCIES=1
|
|
11
|
+
ARG RUNTIME_DEVICE=cuda
|
|
12
|
+
|
|
13
|
+
COPY --from=uv-runtime /uv /uvx /usr/local/bin/
|
|
14
|
+
COPY --from=rust-runtime /usr/local/cargo /opt/citeark/cargo
|
|
15
|
+
COPY --from=rust-runtime /usr/local/rustup /opt/citeark/rustup
|
|
16
|
+
|
|
17
|
+
RUN if [ "${INSTALL_SYSTEM_DEPENDENCIES}" = "1" ]; then \
|
|
18
|
+
apt-get update \
|
|
19
|
+
&& apt-get install -y --no-install-recommends \
|
|
20
|
+
build-essential \
|
|
21
|
+
ca-certificates \
|
|
22
|
+
cmake \
|
|
23
|
+
curl \
|
|
24
|
+
ffmpeg \
|
|
25
|
+
git \
|
|
26
|
+
git-lfs \
|
|
27
|
+
jq \
|
|
28
|
+
libgl1 \
|
|
29
|
+
libglib2.0-0 \
|
|
30
|
+
libgomp1 \
|
|
31
|
+
libsndfile1 \
|
|
32
|
+
ninja-build \
|
|
33
|
+
openssh-client \
|
|
34
|
+
pkg-config \
|
|
35
|
+
poppler-utils \
|
|
36
|
+
python3 \
|
|
37
|
+
python3-pip \
|
|
38
|
+
python3-venv \
|
|
39
|
+
rsync \
|
|
40
|
+
tar \
|
|
41
|
+
util-linux \
|
|
42
|
+
unzip \
|
|
43
|
+
wget \
|
|
44
|
+
&& rm -rf /var/lib/apt/lists/*; \
|
|
45
|
+
fi \
|
|
46
|
+
&& npm install --global \
|
|
47
|
+
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
|
|
48
|
+
"@openai/codex@${CODEX_VERSION}" \
|
|
49
|
+
"opencode-ai@${OPENCODE_VERSION}" \
|
|
50
|
+
&& claude --version \
|
|
51
|
+
&& codex --version \
|
|
52
|
+
&& opencode --version
|
|
53
|
+
|
|
54
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
55
|
+
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
56
|
+
ENV PYTHONUNBUFFERED=1
|
|
57
|
+
ENV UV_PYTHON_INSTALL_DIR=/opt/citeark/managed-python
|
|
58
|
+
ENV UV_PYTHON_BIN_DIR=/usr/local/bin
|
|
59
|
+
ENV CARGO_HOME=/opt/citeark/cargo
|
|
60
|
+
ENV RUSTUP_HOME=/opt/citeark/rustup
|
|
61
|
+
ENV PATH=/opt/citeark/venv/bin:/opt/citeark/cargo/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
62
|
+
ENV PIP_INDEX_URL=https://pypi.org/simple
|
|
63
|
+
ENV HF_ENDPOINT=https://huggingface.co
|
|
64
|
+
|
|
65
|
+
RUN uv python install 3.10 \
|
|
66
|
+
&& uv python install 3.12 --default \
|
|
67
|
+
&& uv --version \
|
|
68
|
+
&& python --version \
|
|
69
|
+
&& python3.10 --version \
|
|
70
|
+
&& python3.12 --version \
|
|
71
|
+
&& rustc --version \
|
|
72
|
+
&& cargo --version
|
|
73
|
+
|
|
74
|
+
COPY docker/claude-code/codex-pro-relay.mjs docker/claude-code/runtime-contract-check.mjs /opt/citeark/
|
|
75
|
+
COPY runtime/requirements-baseline.txt /opt/citeark/requirements-baseline.txt
|
|
76
|
+
COPY runtime/create-runtime-venv.sh /opt/citeark/create-runtime-venv.source.sh
|
|
77
|
+
COPY runtime/install-scientific-runtime.sh /opt/citeark/install-scientific-runtime.sh
|
|
78
|
+
COPY runtime/install-local-cpu-runtime.sh /opt/citeark/install-local-cpu-runtime.sh
|
|
79
|
+
|
|
80
|
+
RUN chmod 0555 /opt/citeark/install-scientific-runtime.sh \
|
|
81
|
+
&& if [ "$RUNTIME_DEVICE" = cpu ]; then \
|
|
82
|
+
bash /opt/citeark/install-local-cpu-runtime.sh; \
|
|
83
|
+
elif [ "$RUNTIME_DEVICE" = cuda ]; then \
|
|
84
|
+
CITEARK_BASELINE_REQUIREMENTS=/opt/citeark/requirements-baseline.txt \
|
|
85
|
+
CITEARK_RUNTIME_VENV_SOURCE=/opt/citeark/create-runtime-venv.source.sh \
|
|
86
|
+
CITEARK_RUNTIME_PROFILE=gcp-scientific-runtime \
|
|
87
|
+
CITEARK_PYTHON_PACKAGE_INDEX=https://pypi.org/simple \
|
|
88
|
+
CITEARK_HUGGINGFACE_ENDPOINT=https://huggingface.co \
|
|
89
|
+
/opt/citeark/install-scientific-runtime.sh; \
|
|
90
|
+
else echo "RUNTIME_DEVICE must be cpu or cuda" >&2; exit 1; fi \
|
|
91
|
+
&& rm -f /opt/citeark/create-runtime-venv.source.sh
|
|
92
|
+
|
|
93
|
+
RUN node /opt/citeark/runtime-contract-check.mjs
|
|
94
|
+
|
|
95
|
+
WORKDIR /job/workspace/repository
|
|
96
|
+
|
|
97
|
+
CMD ["sh", "-lc", "claude --version && codex --version && opencode --version"]
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { writeFile } from "node:fs/promises";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
const DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 10 * 60_000;
|
|
13
|
+
|
|
14
|
+
export function createCodexProRelay({
|
|
15
|
+
upstreamBaseUrl = process.env.CITEARK_PROVIDER_UPSTREAM_BASE_URL
|
|
16
|
+
?? process.env.CITEARK_CODEX_UPSTREAM_BASE_URL,
|
|
17
|
+
apiKey = process.env.OPENAI_API_KEY ?? process.env.CITEARK_API_KEY,
|
|
18
|
+
reasoningMode = process.env.CITEARK_CODEX_REASONING_MODE ?? "standard",
|
|
19
|
+
fetchImpl = fetch,
|
|
20
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
21
|
+
requestTimeoutMs = providerRequestTimeoutMs(),
|
|
22
|
+
paperBudget = budgetEnvironment(),
|
|
23
|
+
} = {}) {
|
|
24
|
+
const upstream = normalizedUpstream(upstreamBaseUrl);
|
|
25
|
+
const providerTimeoutMs = positiveTimeoutMilliseconds(requestTimeoutMs);
|
|
26
|
+
if (!apiKey?.trim()) throw new Error("Codex Pro relay 缺少 OPENAI_API_KEY");
|
|
27
|
+
if (!new Set(["standard", "pro"]).has(reasoningMode)) {
|
|
28
|
+
throw new Error("Codex Pro relay 的推理模式无效");
|
|
29
|
+
}
|
|
30
|
+
const providerFetch = paperBudget
|
|
31
|
+
? createBudgetedProviderFetch({ budget: paperBudget, fetchImpl }) : fetchImpl;
|
|
32
|
+
|
|
33
|
+
return http.createServer(async (request, response) => {
|
|
34
|
+
try {
|
|
35
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
36
|
+
if (request.method === "GET" && requestUrl.pathname === "/health") {
|
|
37
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
38
|
+
response.end('{"ok":true}');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!allowedRequest(request.method, requestUrl.pathname)) {
|
|
42
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
43
|
+
response.end('{"error":"unsupported relay route"}');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rawBody = request.method === "GET"
|
|
48
|
+
? undefined
|
|
49
|
+
: await readRequestBody(request, maxBodyBytes);
|
|
50
|
+
let body = new Set(["/v1/responses", "/v1/chat/completions"]).has(requestUrl.pathname)
|
|
51
|
+
&& reasoningMode === "pro"
|
|
52
|
+
? rewriteCodexRequest(rawBody, reasoningMode)
|
|
53
|
+
: rawBody;
|
|
54
|
+
body = rewriteGeminiToolText(body, upstream, requestUrl.pathname);
|
|
55
|
+
const started = Date.now();
|
|
56
|
+
const headers = forwardedHeaders(request.headers, apiKey);
|
|
57
|
+
if (body) headers.set("content-length", String(body.byteLength));
|
|
58
|
+
const upstreamResponse = await providerFetch(`${upstream}${requestUrl.pathname}${requestUrl.search}`, {
|
|
59
|
+
method: request.method,
|
|
60
|
+
headers,
|
|
61
|
+
body,
|
|
62
|
+
redirect: "manual",
|
|
63
|
+
signal: AbortSignal.timeout(providerTimeoutMs),
|
|
64
|
+
});
|
|
65
|
+
console.log(JSON.stringify({
|
|
66
|
+
event: "codex_pro_relay.response",
|
|
67
|
+
path: requestUrl.pathname,
|
|
68
|
+
status: upstreamResponse.status,
|
|
69
|
+
durationMs: Date.now() - started,
|
|
70
|
+
}));
|
|
71
|
+
// OpenCode retries the generic "Provider returned error" even on HTTP 400.
|
|
72
|
+
// Keep a deterministic malformed tool reference terminal, without logging
|
|
73
|
+
// request/response bodies (which can contain private scientific inputs).
|
|
74
|
+
if (upstreamResponse.status === 400) {
|
|
75
|
+
const errorText = await upstreamResponse.text();
|
|
76
|
+
if (errorText.includes("function_response.response") && errorText.includes("display_name")) {
|
|
77
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
78
|
+
response.end(JSON.stringify({ error: {
|
|
79
|
+
code: 400,
|
|
80
|
+
message: "Gemini rejected an unresolved reference in tool output (invalid tool response).",
|
|
81
|
+
} }));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
response.writeHead(400, responseHeaders(upstreamResponse.headers));
|
|
85
|
+
response.end(errorText);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
response.writeHead(upstreamResponse.status, responseHeaders(upstreamResponse.headers));
|
|
89
|
+
if (!upstreamResponse.body) {
|
|
90
|
+
response.end();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await pipeline(Readable.fromWeb(upstreamResponse.body), response);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.paperBudget === true && paperBudget) {
|
|
96
|
+
await writeFile("/job/execution/paper-budget-exhausted.json", JSON.stringify({
|
|
97
|
+
reason: error.message, at: new Date().toISOString(),
|
|
98
|
+
})).catch(() => {});
|
|
99
|
+
}
|
|
100
|
+
const timedOut = error?.name === "TimeoutError"
|
|
101
|
+
|| error?.code === "ABORT_ERR";
|
|
102
|
+
console.error(JSON.stringify({
|
|
103
|
+
event: "codex_pro_relay.request_failed",
|
|
104
|
+
method: request.method,
|
|
105
|
+
path: request.url,
|
|
106
|
+
failureCategory: timedOut ? "timeout" : "provider",
|
|
107
|
+
message: timedOut
|
|
108
|
+
? `Provider request exceeded ${Math.ceil(providerTimeoutMs / 1000)} seconds`
|
|
109
|
+
: error instanceof Error ? error.message : String(error),
|
|
110
|
+
}));
|
|
111
|
+
if (!response.headersSent) {
|
|
112
|
+
response.writeHead(error?.paperBudget ? 402 : timedOut ? 504 : 502, { "content-type": "application/json" });
|
|
113
|
+
}
|
|
114
|
+
if (!response.writableEnded) {
|
|
115
|
+
response.end(JSON.stringify({
|
|
116
|
+
error: error instanceof Error ? error.message : String(error),
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function budgetEnvironment() {
|
|
124
|
+
if (process.env.CITEARK_PAPER_BUDGET_REQUIRED === "1") {
|
|
125
|
+
return JSON.parse(readFileSync("/job/input/runner-paper-budget.json", "utf8"));
|
|
126
|
+
}
|
|
127
|
+
if (!process.env.CITEARK_PAPER_BUDGET_URL) return null;
|
|
128
|
+
return { url: process.env.CITEARK_PAPER_BUDGET_URL,
|
|
129
|
+
token: process.env.CITEARK_PAPER_BUDGET_TOKEN,
|
|
130
|
+
workerId: process.env.CITEARK_PAPER_BUDGET_WORKER };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The trusted relay reserves before every paid request. The durable paper ledger
|
|
134
|
+
// survives VM preemption, compiler repairs, process crashes and continuations.
|
|
135
|
+
export function isOpenRouterAdmissionRefusal(value) {
|
|
136
|
+
const error = value?.error;
|
|
137
|
+
return error?.code === 402
|
|
138
|
+
&& error.metadata?.reason === "in_flight_budget_exhausted"
|
|
139
|
+
&& error.metadata?.limit_source === "openrouter_in_flight_budget"
|
|
140
|
+
&& error.metadata?.provider_name === null
|
|
141
|
+
&& value.choices === undefined && value.output === undefined && value.usage === undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createBudgetedProviderFetch({ budget, fetchImpl = fetch, modelCatalog } = {}) {
|
|
145
|
+
let modelsPromise;
|
|
146
|
+
// Hold one admission slot through response consumption and settlement. A
|
|
147
|
+
// second request must not exhaust the paper on an in-flight ceiling that is
|
|
148
|
+
// about to be released by the first request's usage receipt.
|
|
149
|
+
let admission = Promise.resolve();
|
|
150
|
+
async function acquire(signal) {
|
|
151
|
+
const previous = admission;
|
|
152
|
+
let unlock;
|
|
153
|
+
admission = new Promise((resolve) => { unlock = resolve; });
|
|
154
|
+
await previous;
|
|
155
|
+
if (signal?.aborted) { unlock(); signal.throwIfAborted(); }
|
|
156
|
+
return unlock;
|
|
157
|
+
}
|
|
158
|
+
async function transact(body) {
|
|
159
|
+
const response = await fetchImpl(budget.url, {
|
|
160
|
+
method: "POST", headers: { "content-type": "application/json", "x-citeark-budget-token": budget.token },
|
|
161
|
+
body: JSON.stringify({ ...body, kind: "model", workerId: budget.workerId }),
|
|
162
|
+
signal: AbortSignal.timeout(20_000),
|
|
163
|
+
});
|
|
164
|
+
if (!response.ok) throw budgetFailure("无法确认论文剩余预算,停止新的模型支出");
|
|
165
|
+
const result = await response.json();
|
|
166
|
+
if (!result.budget?.authorized) throw budgetFailure("论文累计预算不足,停止新的模型支出");
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
return async (url, options = {}) => {
|
|
170
|
+
const endpoint = new URL(url);
|
|
171
|
+
if ((options.method ?? "GET") === "GET") return fetchImpl(url, options);
|
|
172
|
+
if (endpoint.hostname !== "openrouter.ai"
|
|
173
|
+
|| !/\/(chat\/completions|responses)$/.test(endpoint.pathname)) {
|
|
174
|
+
throw budgetFailure("该模型接口尚无可执行的费用上限,禁止启动付费请求");
|
|
175
|
+
}
|
|
176
|
+
if (!modelsPromise) modelsPromise = modelCatalog ? Promise.resolve(modelCatalog)
|
|
177
|
+
: fetchImpl("https://openrouter.ai/api/v1/models", { signal: AbortSignal.timeout(15_000) })
|
|
178
|
+
.then(async (response) => { if (!response.ok) throw budgetFailure("模型价格不可用"); return (await response.json()).data; })
|
|
179
|
+
.catch((error) => { modelsPromise = null; throw budgetFailure(error.message); });
|
|
180
|
+
const payload = JSON.parse(Buffer.from(options.body).toString("utf8"));
|
|
181
|
+
const models = await modelsPromise;
|
|
182
|
+
const quote = quoteBudgetedModelRequest(payload, models);
|
|
183
|
+
const unlock = await acquire(options.signal);
|
|
184
|
+
let streaming = false;
|
|
185
|
+
try {
|
|
186
|
+
const key = `model:${randomUUID()}`;
|
|
187
|
+
await transact({ action: "reserve", key, maximumCny: quote.maximumCny,
|
|
188
|
+
details: { model: payload.model, maximumTokens: quote.maximumTokens, source: "openrouter-catalog-with-price-ceiling" } });
|
|
189
|
+
// If delivery/completion is uncertain, retain the full reservation.
|
|
190
|
+
const headers = new Headers(options.headers);
|
|
191
|
+
headers.delete("content-length");
|
|
192
|
+
const response = await fetchImpl(url, { ...options, headers, body: JSON.stringify(quote.payload) });
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
// Only the router's explicit admission refusal is known to precede
|
|
195
|
+
// inference. Generic 402s and missing receipts do not prove zero spend.
|
|
196
|
+
if (response.status === 402 && (response.headers.get("content-type") ?? "").includes("application/json")) {
|
|
197
|
+
let rejected;
|
|
198
|
+
try { rejected = await response.clone().json(); } catch { /* Keep unknown holds. */ }
|
|
199
|
+
const generationId = response.headers.get("x-generation-id");
|
|
200
|
+
if (isOpenRouterAdmissionRefusal(rejected) && /^gen-[a-zA-Z0-9_-]{1,240}$/.test(generationId ?? "")) {
|
|
201
|
+
await transact({ action: "settle", key, actualCny: 0, details: {
|
|
202
|
+
providerCostUsd: 0, source: "openrouter-admission-refusal", generationId,
|
|
203
|
+
status: 402, reason: rejected.error.metadata.reason,
|
|
204
|
+
limitSource: rejected.error.metadata.limit_source,
|
|
205
|
+
} });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return response;
|
|
209
|
+
}
|
|
210
|
+
let cost = null;
|
|
211
|
+
let generationId = null;
|
|
212
|
+
let costSource = "provider-response";
|
|
213
|
+
function observe(line) {
|
|
214
|
+
try {
|
|
215
|
+
const value = JSON.parse(line.replace(/^data:\s*/, ""));
|
|
216
|
+
const candidateId = value.id ?? value.response?.id;
|
|
217
|
+
if (typeof candidateId === "string" && /^gen-[a-zA-Z0-9_-]{1,240}$/.test(candidateId)) generationId = candidateId;
|
|
218
|
+
const usage = value.usage ?? value.response?.usage;
|
|
219
|
+
if (typeof usage?.cost === "number" && Number.isFinite(usage.cost) && usage.cost >= 0) cost = usage.cost;
|
|
220
|
+
} catch { /* SSE comments and protocol delimiters are not usage. */ }
|
|
221
|
+
}
|
|
222
|
+
async function settle() {
|
|
223
|
+
// A completed response can omit usage. Reconcile the exact provider
|
|
224
|
+
// receipt once; never estimate actual spend or release an unknown hold.
|
|
225
|
+
if (cost === null && generationId) {
|
|
226
|
+
try {
|
|
227
|
+
const receipt = await fetchImpl(`https://openrouter.ai/api/v1/generation?id=${encodeURIComponent(generationId)}`, {
|
|
228
|
+
headers: { Authorization: headers.get("Authorization") ?? "" },
|
|
229
|
+
signal: AbortSignal.timeout(5_000),
|
|
230
|
+
});
|
|
231
|
+
if (receipt.ok) {
|
|
232
|
+
const data = (await receipt.json()).data;
|
|
233
|
+
if (data?.id === generationId && typeof data.total_cost === "number"
|
|
234
|
+
&& Number.isFinite(data.total_cost) && data.total_cost >= 0) {
|
|
235
|
+
cost = data.total_cost;
|
|
236
|
+
costSource = "provider-generation-receipt";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch { /* Unavailable receipts retain the original reservation. */ }
|
|
240
|
+
}
|
|
241
|
+
if (cost === null) return;
|
|
242
|
+
await transact({ action: "settle", key, actualCny: monetaryCny(cost),
|
|
243
|
+
details: { providerCostUsd: cost, source: costSource, ...(generationId ? { generationId } : {}) } });
|
|
244
|
+
}
|
|
245
|
+
if (!response.body) return response;
|
|
246
|
+
if (!(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
247
|
+
const bytes = await response.arrayBuffer();
|
|
248
|
+
observe(Buffer.from(bytes).toString("utf8"));
|
|
249
|
+
await settle();
|
|
250
|
+
return new Response(bytes, { status: response.status, headers: response.headers });
|
|
251
|
+
}
|
|
252
|
+
let pending = "";
|
|
253
|
+
const decoder = new TextDecoder();
|
|
254
|
+
const stream = response.body.pipeThrough(new TransformStream({
|
|
255
|
+
transform(chunk, controller) {
|
|
256
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
257
|
+
const lines = pending.split("\n"); pending = lines.pop();
|
|
258
|
+
for (const line of lines) observe(line);
|
|
259
|
+
if (pending.length > 32 * 1024 * 1024) throw budgetFailure("模型响应超过预算审计边界");
|
|
260
|
+
controller.enqueue(chunk);
|
|
261
|
+
},
|
|
262
|
+
async flush() { observe(pending + decoder.decode()); await settle(); },
|
|
263
|
+
}));
|
|
264
|
+
const reader = stream.getReader();
|
|
265
|
+
const body = new ReadableStream({
|
|
266
|
+
async pull(controller) {
|
|
267
|
+
try {
|
|
268
|
+
const chunk = await reader.read();
|
|
269
|
+
if (chunk.done) { unlock(); controller.close(); }
|
|
270
|
+
else controller.enqueue(chunk.value);
|
|
271
|
+
} catch (error) { unlock(); controller.error(error); }
|
|
272
|
+
},
|
|
273
|
+
async cancel(reason) {
|
|
274
|
+
try { await reader.cancel(reason); } finally { unlock(); }
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
streaming = true;
|
|
278
|
+
return new Response(body, { status: response.status, headers: response.headers });
|
|
279
|
+
} finally {
|
|
280
|
+
if (!streaming) unlock();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function quoteBudgetedModelRequest(payload, models) {
|
|
286
|
+
const model = models?.find((item) => item.id === payload.model);
|
|
287
|
+
const tiers = [model?.pricing, ...(Array.isArray(model?.pricing?.overrides) ? model.pricing.overrides : [])];
|
|
288
|
+
const prompt = Math.max(...tiers.map((p) => Number(p?.prompt ?? model?.pricing?.prompt)));
|
|
289
|
+
const completion = Math.max(...tiers.map((p) => Number(p?.completion ?? model?.pricing?.completion)), Number(model?.pricing?.internal_reasoning ?? 0));
|
|
290
|
+
const context = Number(model?.context_length);
|
|
291
|
+
if (!model || !Number.isFinite(prompt) || prompt < 0 || !Number.isFinite(completion)
|
|
292
|
+
|| completion < 0 || !Number.isInteger(context) || context < 1) throw budgetFailure("模型价格或上下文上限缺失");
|
|
293
|
+
if (payload.plugins?.length || payload.web_search_options || payload.audio
|
|
294
|
+
|| payload.modalities?.some((kind) => kind !== "text") || (payload.n ?? 1) !== 1
|
|
295
|
+
|| payload.models?.length) throw budgetFailure("附加模型服务缺少可验证的费用上限");
|
|
296
|
+
const nonTokenCharges = Object.entries(model.pricing ?? {}).filter(([name, value]) =>
|
|
297
|
+
!new Set(["prompt", "completion", "request", "input_cache_read", "input_cache_write", "discount", "overrides", "image", "audio", "input_audio_cache", "web_search", "internal_reasoning"]).has(name)
|
|
298
|
+
&& Number(value) > 0);
|
|
299
|
+
if (Number(model.pricing?.image ?? 0) > prompt || Number(model.pricing?.audio ?? 0) > prompt
|
|
300
|
+
|| Number(model.pricing?.input_cache_write ?? 0) > prompt || Number(model.pricing?.input_cache_read ?? 0) > prompt) {
|
|
301
|
+
throw budgetFailure("输入或缓存单价超出令牌费用上限");
|
|
302
|
+
}
|
|
303
|
+
if (payload.tools?.some((tool) => tool.type !== "function" && tool.type !== "custom")) throw budgetFailure("付费内置工具缺少费用上限");
|
|
304
|
+
if (nonTokenCharges.length) throw budgetFailure("模型包含尚未纳入上限的附加计费项目");
|
|
305
|
+
const requested = payload.max_output_tokens ?? payload.max_completion_tokens ?? payload.max_tokens ?? 8192;
|
|
306
|
+
const maximumTokens = Math.min(Number(requested), Number(model.top_provider?.max_completion_tokens) || context, context);
|
|
307
|
+
if (!Number.isInteger(maximumTokens) || maximumTokens < 1) throw budgetFailure("模型输出上限无效");
|
|
308
|
+
const perRequest = Number(model.pricing?.request ?? 0);
|
|
309
|
+
if (!Number.isFinite(perRequest) || perRequest < 0) throw budgetFailure("模型请求价格缺失");
|
|
310
|
+
// A full context is an upper bound even for images/tool framing, independent
|
|
311
|
+
// of tokenizer guesses. Routing is restricted to these per-token ceilings.
|
|
312
|
+
const maximumCny = monetaryCny(context * prompt + maximumTokens * completion + perRequest);
|
|
313
|
+
const bounded = { ...payload, provider: { ...payload.provider, require_parameters: true,
|
|
314
|
+
max_price: { prompt: prompt * 1e6, completion: completion * 1e6, request: perRequest } } };
|
|
315
|
+
// OpenCode 1.18.14 and Codex 0.144.1 add this presentation default to GPT requests,
|
|
316
|
+
// even when the routed model does not support it. Omit only that known
|
|
317
|
+
// optional default when the catalog explicitly excludes it; keep required
|
|
318
|
+
// reasoning, tools, output limits and strict provider matching intact.
|
|
319
|
+
if (/^openai\/gpt-/.test(payload.model)
|
|
320
|
+
&& Array.isArray(model.supported_parameters) && !model.supported_parameters.includes("verbosity")) {
|
|
321
|
+
if (payload.verbosity === "low") delete bounded.verbosity;
|
|
322
|
+
if (payload.text?.verbosity === "low") {
|
|
323
|
+
bounded.text = { ...payload.text };
|
|
324
|
+
delete bounded.text.verbosity;
|
|
325
|
+
if (Object.keys(bounded.text).length === 0) delete bounded.text;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Codex emits a boolean parallel-tool hint even when the routed API does
|
|
329
|
+
// not support that parameter. For Responses, use the provider default
|
|
330
|
+
// when its catalog explicitly excludes the hint; tools remain intact.
|
|
331
|
+
if (/^openai\/gpt-/.test(payload.model) && payload.input !== undefined
|
|
332
|
+
&& typeof payload.parallel_tool_calls === "boolean"
|
|
333
|
+
&& Array.isArray(model.supported_parameters) && !model.supported_parameters.includes("parallel_tool_calls")) {
|
|
334
|
+
delete bounded.parallel_tool_calls;
|
|
335
|
+
}
|
|
336
|
+
if (payload.input !== undefined) bounded.max_output_tokens = maximumTokens;
|
|
337
|
+
else { bounded.max_tokens = maximumTokens; if (bounded.max_completion_tokens !== undefined) bounded.max_completion_tokens = maximumTokens; }
|
|
338
|
+
if (payload.stream && payload.input === undefined) bounded.stream_options = { ...payload.stream_options, include_usage: true };
|
|
339
|
+
return { maximumCny, maximumTokens, payload: bounded };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function monetaryCny(usd) { return Math.ceil(usd * 7.2 * 1.15 * 1e6) / 1e6; }
|
|
343
|
+
function budgetFailure(message) {
|
|
344
|
+
return Object.assign(new Error(message), { paperBudget: true,
|
|
345
|
+
failureCode: "paper.budget_exhausted", failureCategory: "budget", retryable: false });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function rewriteGeminiToolText(rawBody, upstream, pathname) {
|
|
349
|
+
if (new URL(upstream).hostname !== "openrouter.ai" || pathname !== "/v1/chat/completions") return rawBody;
|
|
350
|
+
const payload = JSON.parse(Buffer.from(rawBody ?? []).toString("utf8"));
|
|
351
|
+
if (!/^google\/gemini-/i.test(payload.model ?? "") || !Array.isArray(payload.messages)) return rawBody;
|
|
352
|
+
let changed = false;
|
|
353
|
+
const messages = payload.messages.map((message) => {
|
|
354
|
+
if (message.role !== "tool" || typeof message.content !== "string") return message;
|
|
355
|
+
changed = true;
|
|
356
|
+
// Preserve the literal result inside a string: Gemini interprets a top-level
|
|
357
|
+
// parsed $ref as a multimodal part reference, including nested JSON Schemas.
|
|
358
|
+
return { ...message, content: JSON.stringify({ text: message.content }) };
|
|
359
|
+
});
|
|
360
|
+
return changed ? Buffer.from(JSON.stringify({ ...payload, messages })) : rawBody;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function providerRequestTimeoutMs() {
|
|
364
|
+
const seconds = process.env.CITEARK_PROVIDER_REQUEST_TIMEOUT_SECONDS;
|
|
365
|
+
if (seconds === undefined || seconds === "") return DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
|
|
366
|
+
const numeric = Number(seconds);
|
|
367
|
+
if (!Number.isInteger(numeric) || numeric < 1 || numeric > 30 * 60) {
|
|
368
|
+
throw new Error("Provider 请求超时必须是 1–1800 秒的整数");
|
|
369
|
+
}
|
|
370
|
+
return numeric * 1000;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function positiveTimeoutMilliseconds(value) {
|
|
374
|
+
if (!Number.isInteger(value) || value < 1 || value > 30 * 60_000) {
|
|
375
|
+
throw new Error("Provider 请求超时必须是 1–1800000 毫秒的整数");
|
|
376
|
+
}
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function rewriteCodexRequest(rawBody, reasoningMode) {
|
|
381
|
+
let payload;
|
|
382
|
+
try {
|
|
383
|
+
payload = JSON.parse(Buffer.from(rawBody ?? []).toString("utf8"));
|
|
384
|
+
} catch {
|
|
385
|
+
throw new Error("Codex Responses 请求不是有效 JSON");
|
|
386
|
+
}
|
|
387
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
388
|
+
throw new Error("Codex Responses 请求必须是对象");
|
|
389
|
+
}
|
|
390
|
+
const reasoning = payload.reasoning && typeof payload.reasoning === "object" && !Array.isArray(payload.reasoning)
|
|
391
|
+
? payload.reasoning
|
|
392
|
+
: {};
|
|
393
|
+
return Buffer.from(JSON.stringify({
|
|
394
|
+
...payload,
|
|
395
|
+
reasoning: { ...reasoning, mode: reasoningMode },
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function allowedRequest(method, pathname) {
|
|
400
|
+
if (method === "GET" && pathname === "/v1/models") return true;
|
|
401
|
+
return method === "POST" && new Set([
|
|
402
|
+
"/v1/responses",
|
|
403
|
+
"/v1/responses/compact",
|
|
404
|
+
"/v1/chat/completions",
|
|
405
|
+
]).has(pathname);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizedUpstream(value) {
|
|
409
|
+
if (!value?.trim()) throw new Error("Codex Pro relay 缺少上游 API 地址");
|
|
410
|
+
const url = new URL(value);
|
|
411
|
+
if (!new Set(["http:", "https:"]).has(url.protocol)) {
|
|
412
|
+
throw new Error("Codex Pro relay 上游只支持 HTTP(S)");
|
|
413
|
+
}
|
|
414
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
415
|
+
throw new Error("Codex Pro relay 上游地址不能包含凭据、查询参数或片段");
|
|
416
|
+
}
|
|
417
|
+
return url.toString().replace(/\/$/, "").replace(/\/v1$/, "");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function forwardedHeaders(source, apiKey) {
|
|
421
|
+
const headers = new Headers();
|
|
422
|
+
for (const [name, rawValue] of Object.entries(source)) {
|
|
423
|
+
if (!rawValue) continue;
|
|
424
|
+
const lower = name.toLowerCase();
|
|
425
|
+
if (new Set(["host", "authorization", "connection", "content-length", "transfer-encoding"]).has(lower)) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
headers.set(name, Array.isArray(rawValue) ? rawValue.join(", ") : rawValue);
|
|
429
|
+
}
|
|
430
|
+
headers.set("authorization", `Bearer ${apiKey.trim()}`);
|
|
431
|
+
headers.set("content-type", "application/json");
|
|
432
|
+
return headers;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function responseHeaders(source) {
|
|
436
|
+
const headers = {};
|
|
437
|
+
for (const [name, value] of source.entries()) {
|
|
438
|
+
if (new Set(["connection", "content-length", "content-encoding", "transfer-encoding"]).has(name.toLowerCase())) {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
headers[name] = value;
|
|
442
|
+
}
|
|
443
|
+
return headers;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function readRequestBody(request, maxBodyBytes) {
|
|
447
|
+
const declared = Number(request.headers["content-length"] ?? 0);
|
|
448
|
+
if (declared > maxBodyBytes) throw new Error("Codex 请求体过大");
|
|
449
|
+
const chunks = [];
|
|
450
|
+
let total = 0;
|
|
451
|
+
for await (const chunk of request) {
|
|
452
|
+
const buffer = Buffer.from(chunk);
|
|
453
|
+
total += buffer.byteLength;
|
|
454
|
+
if (total > maxBodyBytes) throw new Error("Codex 请求体过大");
|
|
455
|
+
chunks.push(buffer);
|
|
456
|
+
}
|
|
457
|
+
return Buffer.concat(chunks);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
461
|
+
const port = Number(process.env.CITEARK_CODEX_RELAY_PORT ?? 4317);
|
|
462
|
+
const server = createCodexProRelay();
|
|
463
|
+
server.listen(port, "127.0.0.1", () => {
|
|
464
|
+
console.log(JSON.stringify({ event: "codex_pro_relay.ready", port }));
|
|
465
|
+
});
|
|
466
|
+
}
|