@grada-run/grada 0.0.1 → 0.33.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/.github/workflows/deploy-docs.yml +37 -0
- package/.github/workflows/e2e.yml +73 -0
- package/.github/workflows/iac-validation.yml +303 -0
- package/.github/workflows/publish.yml +68 -0
- package/.github/workflows/sync-bedrock-models.yml +57 -0
- package/.github/workflows/test.yml +43 -0
- package/.muserules +31 -0
- package/LICENSE +21 -0
- package/README.md +193 -3
- package/apps/docs/.astro/collections/docs.schema.json +644 -0
- package/apps/docs/.astro/content-assets.mjs +4 -0
- package/apps/docs/.astro/content-modules.mjs +4 -0
- package/apps/docs/.astro/content.d.ts +179 -0
- package/apps/docs/.astro/data-store.json +1 -0
- package/apps/docs/.astro/dev.json +14 -0
- package/apps/docs/.astro/settings.json +5 -0
- package/apps/docs/.astro/types.d.ts +2 -0
- package/apps/docs/astro.config.mjs +97 -0
- package/apps/docs/package.json +17 -0
- package/apps/docs/src/content/docs/adrs/0001-s3-native-state-locking.md +37 -0
- package/apps/docs/src/content/docs/adrs/0002-eject-mechanism-pure-iac.md +39 -0
- package/apps/docs/src/content/docs/adrs/0003-sync-ai-context-strategy.md +48 -0
- package/apps/docs/src/content/docs/adrs/0004-iac-driven-diagnostic-context.md +37 -0
- package/apps/docs/src/content/docs/adrs/0005-ecs-fargate-alb-runtime-target.md +38 -0
- package/apps/docs/src/content/docs/adrs/0006-github-oidc-no-stored-keys.md +37 -0
- package/apps/docs/src/content/docs/adrs/0007-framework-detection-with-fallback.md +37 -0
- package/apps/docs/src/content/docs/adrs/0008-secrets-names-in-git-values-in-aws.md +37 -0
- package/apps/docs/src/content/docs/adrs/0009-regenerate-with-backup-on-rerun.md +37 -0
- package/apps/docs/src/content/docs/adrs/0010-advisory-only-security-scans.md +37 -0
- package/apps/docs/src/content/docs/cli/add.md +85 -0
- package/apps/docs/src/content/docs/cli/apply.md +35 -0
- package/apps/docs/src/content/docs/cli/db.md +200 -0
- package/apps/docs/src/content/docs/cli/destroy.md +35 -0
- package/apps/docs/src/content/docs/cli/diagnose.md +37 -0
- package/apps/docs/src/content/docs/cli/doctor.md +28 -0
- package/apps/docs/src/content/docs/cli/domain.md +57 -0
- package/apps/docs/src/content/docs/cli/drift.md +40 -0
- package/apps/docs/src/content/docs/cli/eject.md +33 -0
- package/apps/docs/src/content/docs/cli/exec.md +49 -0
- package/apps/docs/src/content/docs/cli/gc.md +37 -0
- package/apps/docs/src/content/docs/cli/init.md +72 -0
- package/apps/docs/src/content/docs/cli/logs.md +39 -0
- package/apps/docs/src/content/docs/cli/rollback.md +51 -0
- package/apps/docs/src/content/docs/cli/secrets.md +73 -0
- package/apps/docs/src/content/docs/cli/sleep.md +53 -0
- package/apps/docs/src/content/docs/cli/status.md +34 -0
- package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
- package/apps/docs/src/content/docs/guides/architecture.md +87 -0
- package/apps/docs/src/content/docs/guides/aws-credentials.md +72 -0
- package/apps/docs/src/content/docs/guides/background-workers.md +45 -0
- package/apps/docs/src/content/docs/guides/cicd-pipeline.md +64 -0
- package/apps/docs/src/content/docs/guides/database-connections.md +64 -0
- package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
- package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
- package/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md +39 -0
- package/apps/docs/src/content/docs/guides/examples.md +50 -0
- package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
- package/apps/docs/src/content/docs/guides/headless.md +87 -0
- package/apps/docs/src/content/docs/guides/quickstart.md +52 -0
- package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
- package/apps/docs/src/content/docs/guides/secrets-management.md +83 -0
- package/apps/docs/src/content/docs/guides/understanding-your-bill.md +63 -0
- package/apps/docs/src/content/docs/index.mdx +103 -0
- package/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md +55 -0
- package/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md +41 -0
- package/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md +51 -0
- package/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md +63 -0
- package/apps/docs/src/content/docs/roadmap.md +99 -0
- package/apps/docs/src/content/docs/testing-strategy.md +37 -0
- package/apps/docs/src/content.config.ts +7 -0
- package/apps/docs/src/custom.css +14 -0
- package/apps/docs/tsconfig.json +6 -0
- package/bin/cli.js +140 -0
- package/package.json +107 -7
- package/scripts/sync-bedrock-models.js +22 -0
- package/scripts/test-iac.js +261 -0
- package/specs/add-redis-sqs-bedrock.md +128 -0
- package/specs/add-storage-dynamodb.md +106 -0
- package/specs/bedrock-model-catalog.md +131 -0
- package/specs/ci-pipeline.md +17 -0
- package/specs/cost-transparency.md +115 -0
- package/specs/custom-domains-and-ses.md +153 -0
- package/specs/database-suite-expansion.md +151 -0
- package/specs/db-connect.md +69 -0
- package/specs/db-lifecycle-migrations.md +159 -0
- package/specs/dependency-aware-init.md +176 -0
- package/specs/deployment-safety.md +170 -0
- package/specs/diagnose.md +16 -0
- package/specs/docs-hub.md +16 -0
- package/specs/dx-polish.md +46 -0
- package/specs/e2e-testing.md +50 -0
- package/specs/exec.md +25 -0
- package/specs/finops-cron-drift.md +161 -0
- package/specs/gc.md +26 -0
- package/specs/integration-suite.md +16 -0
- package/specs/logs.md +32 -0
- package/specs/rollback-live-polling.md +40 -0
- package/specs/secrets-pull-audit.md +51 -0
- package/specs/serverless-lambda-target.md +133 -0
- package/specs/status.md +31 -0
- package/specs/telemetry-and-spawn-hardening.md +69 -0
- package/specs/telemetry-hardening.md +35 -0
- package/specs/test-suite-deduplication.md +42 -0
- package/src/commands/add.js +1111 -0
- package/src/commands/apply.js +214 -0
- package/src/commands/db/backup.js +229 -0
- package/src/commands/db/connect.js +304 -0
- package/src/commands/db/enable-vector.js +344 -0
- package/src/commands/db/import.js +604 -0
- package/src/commands/db/migrate.js +477 -0
- package/src/commands/db/restore.js +361 -0
- package/src/commands/db.js +87 -0
- package/src/commands/destroy.js +217 -0
- package/src/commands/diagnose.js +460 -0
- package/src/commands/doctor.js +109 -0
- package/src/commands/domain.js +685 -0
- package/src/commands/drift.js +243 -0
- package/src/commands/eject.js +130 -0
- package/src/commands/exec.js +222 -0
- package/src/commands/gc.js +250 -0
- package/src/commands/init.js +649 -0
- package/src/commands/logs.js +256 -0
- package/src/commands/rollback.js +323 -0
- package/src/commands/secrets.js +485 -0
- package/src/commands/sleep.js +347 -0
- package/src/commands/status.js +309 -0
- package/src/commands/sync-ai.js +115 -0
- package/src/commands/wake.js +337 -0
- package/src/core/parser.js +128 -0
- package/src/core/telemetry.js +244 -0
- package/src/data/bedrock-models.json +896 -0
- package/src/utils/addons.js +126 -0
- package/src/utils/ai-rules.js +59 -0
- package/src/utils/args.js +91 -0
- package/src/utils/aws.js +178 -0
- package/src/utils/backup.js +69 -0
- package/src/utils/bedrock-catalog.js +511 -0
- package/src/utils/capabilities.js +500 -0
- package/src/utils/command.js +78 -0
- package/src/utils/db-tunnel.js +164 -0
- package/src/utils/detector.js +298 -0
- package/src/utils/dockerCompose.js +65 -0
- package/src/utils/domains.js +73 -0
- package/src/utils/ecs-runner.js +289 -0
- package/src/utils/ecs.js +92 -0
- package/src/utils/frameworks.js +55 -0
- package/src/utils/generator.js +527 -0
- package/src/utils/hcl.js +426 -0
- package/src/utils/lambda-ecr.js +185 -0
- package/src/utils/prompts.js +278 -0
- package/src/utils/rds.js +131 -0
- package/src/utils/resolvers.js +174 -0
- package/src/utils/sleep-state.js +140 -0
- package/src/utils/sleep-targets.js +139 -0
- package/src/utils/system.js +42 -0
- package/src/utils/terraform.js +70 -0
- package/src/utils/visualizer.js +381 -0
- package/src/utils/warnings.js +49 -0
- package/templates/README.md +150 -0
- package/templates/docker/django.Dockerfile +40 -0
- package/templates/docker/go.Dockerfile +23 -0
- package/templates/docker/nestjs.Dockerfile +33 -0
- package/templates/docker/nextjs.Dockerfile +55 -0
- package/templates/docker/node.Dockerfile +24 -0
- package/templates/docker/nuxt.Dockerfile +47 -0
- package/templates/docker/python.Dockerfile +38 -0
- package/templates/docker/rails.Dockerfile +59 -0
- package/templates/docker/static.Dockerfile +32 -0
- package/templates/docker/svelte.Dockerfile +52 -0
- package/templates/github/deploy-lambda.yml +120 -0
- package/templates/github/deploy.yml +138 -0
- package/templates/github/drift.yml +112 -0
- package/templates/github/preview-lambda.yml +86 -0
- package/templates/github/preview.yml +69 -0
- package/templates/github/teardown.yml +43 -0
- package/templates/terraform/addons/bedrock.tf +34 -0
- package/templates/terraform/addons/cron-lambda.tf +78 -0
- package/templates/terraform/addons/cron.tf +101 -0
- package/templates/terraform/addons/dynamodb.tf +73 -0
- package/templates/terraform/addons/redis.tf +64 -0
- package/templates/terraform/addons/s3.tf +143 -0
- package/templates/terraform/addons/ses.tf +73 -0
- package/templates/terraform/addons/sqs.tf +67 -0
- package/templates/terraform/backend.tf +22 -0
- package/templates/terraform/cloudfront-lambda.tf +80 -0
- package/templates/terraform/cloudfront.tf +80 -0
- package/templates/terraform/database-aurora-postgresql.tf +92 -0
- package/templates/terraform/database-mysql.tf +72 -0
- package/templates/terraform/database.tf +71 -0
- package/templates/terraform/main-lambda.tf +229 -0
- package/templates/terraform/main.tf +296 -0
- package/templates/terraform/network.tf +95 -0
- package/templates/terraform/oidc.tf +64 -0
- package/templates/terraform/secrets.tf +31 -0
- package/templates/terraform/worker.tf +69 -0
- package/tests/__snapshots__/generator.test.js.snap +9633 -0
- package/tests/add.test.js +2018 -0
- package/tests/ai.test.js +91 -0
- package/tests/apply.test.js +446 -0
- package/tests/args.test.js +86 -0
- package/tests/aws.test.js +244 -0
- package/tests/capabilities.test.js +304 -0
- package/tests/cli.test.js +29 -0
- package/tests/command.test.js +99 -0
- package/tests/commands-import.test.js +74 -0
- package/tests/db.test.js +2673 -0
- package/tests/destroy.test.js +365 -0
- package/tests/detector.test.js +79 -0
- package/tests/diagnose.test.js +765 -0
- package/tests/doctor.test.js +195 -0
- package/tests/domain.test.js +882 -0
- package/tests/drift.test.js +228 -0
- package/tests/e2e/helpers.js +122 -0
- package/tests/e2e/tier0.e2e.test.js +142 -0
- package/tests/e2e/tier1.live.e2e.test.js +118 -0
- package/tests/ecs.test.js +130 -0
- package/tests/eject.test.js +65 -0
- package/tests/exec.test.js +366 -0
- package/tests/gc.test.js +466 -0
- package/tests/generator.test.js +794 -0
- package/tests/headless.test.js +491 -0
- package/tests/helpers/clack.js +103 -0
- package/tests/helpers/console.js +41 -0
- package/tests/helpers/telemetry.js +37 -0
- package/tests/helpers/tmpdir.js +36 -0
- package/tests/lambda-ecr.test.js +185 -0
- package/tests/logs.test.js +436 -0
- package/tests/parser.test.js +160 -0
- package/tests/rds.test.js +244 -0
- package/tests/resolvers.test.js +279 -0
- package/tests/rollback.test.js +670 -0
- package/tests/secrets.test.js +729 -0
- package/tests/sleep-wake.test.js +998 -0
- package/tests/status.test.js +356 -0
- package/tests/system.test.js +70 -0
- package/tests/telemetry.test.js +520 -0
- package/tests/terraform.test.js +84 -0
- package/tests/visualizer.test.js +480 -0
- package/vitest.config.js +10 -0
- package/vitest.e2e.tier0.config.js +8 -0
- package/vitest.e2e.tier1.config.js +8 -0
- package/index.js +0 -2
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { log } from '@clack/prompts';
|
|
6
|
+
import {
|
|
7
|
+
BedrockClient,
|
|
8
|
+
ListInferenceProfilesCommand,
|
|
9
|
+
ListFoundationModelsCommand,
|
|
10
|
+
} from '@aws-sdk/client-bedrock';
|
|
11
|
+
|
|
12
|
+
export const FALLBACK_BEDROCK_MODEL = 'us.anthropic.claude-sonnet-4-6';
|
|
13
|
+
export const GENERIC_MODEL_HINT = 'Live AWS Bedrock model';
|
|
14
|
+
|
|
15
|
+
// Keep in sync with MODEL_ID_RE in src/commands/add.js (duplicated here to
|
|
16
|
+
// avoid a module cycle between add.js and this loader).
|
|
17
|
+
const MODEL_ID_PATTERN = /^[a-zA-Z0-9._:-]+$/;
|
|
18
|
+
|
|
19
|
+
export const FALLBACK_CATALOG = {
|
|
20
|
+
updatedAt: '2026-01-01',
|
|
21
|
+
defaultModelId: FALLBACK_BEDROCK_MODEL,
|
|
22
|
+
providers: [
|
|
23
|
+
{
|
|
24
|
+
provider: 'Anthropic',
|
|
25
|
+
models: [
|
|
26
|
+
{
|
|
27
|
+
id: FALLBACK_BEDROCK_MODEL,
|
|
28
|
+
name: 'Claude Sonnet 4.6',
|
|
29
|
+
hint: 'Recommended default — standard temperature/top_p SDK compatibility',
|
|
30
|
+
recommended: true,
|
|
31
|
+
scopes: ['us'],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
+
const __dirname = path.dirname(__filename);
|
|
40
|
+
const BUNDLED_CATALOG_PATH = path.join(__dirname, '../data/bedrock-models.json');
|
|
41
|
+
|
|
42
|
+
const PROVIDER_ALIASES = {
|
|
43
|
+
anthropic: 'Anthropic',
|
|
44
|
+
openai: 'OpenAI',
|
|
45
|
+
deepseek: 'DeepSeek',
|
|
46
|
+
meta: 'Meta',
|
|
47
|
+
amazon: 'Amazon',
|
|
48
|
+
google: 'Google',
|
|
49
|
+
mistral: 'Mistral AI',
|
|
50
|
+
'mistral ai': 'Mistral AI',
|
|
51
|
+
xai: 'xAI',
|
|
52
|
+
moonshot: 'Moonshot AI',
|
|
53
|
+
'moonshot ai': 'Moonshot AI',
|
|
54
|
+
moonshotai: 'Moonshot AI',
|
|
55
|
+
cohere: 'Cohere',
|
|
56
|
+
stability: 'Stability AI',
|
|
57
|
+
twelvelabs: 'TwelveLabs',
|
|
58
|
+
writer: 'Writer',
|
|
59
|
+
nvidia: 'NVIDIA',
|
|
60
|
+
qwen: 'Qwen',
|
|
61
|
+
minimax: 'MiniMax',
|
|
62
|
+
zai: 'Zhipu AI',
|
|
63
|
+
z: 'Zhipu AI',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function deepClone(value) {
|
|
67
|
+
return JSON.parse(JSON.stringify(value));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Returns a durable tier + API-compatibility hint for a model family,
|
|
71
|
+
// matched case-insensitively on the normalized provider and model ID
|
|
72
|
+
// substrings. Unknown families fall back to GENERIC_MODEL_HINT so the
|
|
73
|
+
// maintainer sync can flag models missing hint rules.
|
|
74
|
+
export function generateHintForModel(provider, modelId, { defaultModelId = FALLBACK_BEDROCK_MODEL } = {}) {
|
|
75
|
+
if (modelId === defaultModelId) {
|
|
76
|
+
return 'Recommended default — standard temperature/top_p SDK compatibility';
|
|
77
|
+
}
|
|
78
|
+
const id = String(modelId ?? '').toLowerCase();
|
|
79
|
+
const has = (...substrings) => substrings.every((part) => id.includes(part));
|
|
80
|
+
const hasAny = (...substrings) => substrings.some((part) => id.includes(part));
|
|
81
|
+
|
|
82
|
+
switch (normalizeProviderName(provider)) {
|
|
83
|
+
case 'Anthropic':
|
|
84
|
+
if (has('fable')) return 'Mythos-tier autonomous workflows & agents (adaptive thinking on; omit temperature)';
|
|
85
|
+
if (has('opus-5') || (has('opus') && has('-5'))) {
|
|
86
|
+
return 'Opus-tier long-horizon coding & agents (adaptive thinking on; omit temperature)';
|
|
87
|
+
}
|
|
88
|
+
if (has('sonnet-5') || (has('sonnet') && has('-5'))) {
|
|
89
|
+
return 'Sonnet-tier coding & agents (adaptive thinking on; omit temperature)';
|
|
90
|
+
}
|
|
91
|
+
if (has('opus')) return 'Opus 4-series deep reasoning & engineering (standard temperature/top_p compatible)';
|
|
92
|
+
if (has('sonnet')) return 'Balanced coding & reasoning (standard temperature/top_p compatible)';
|
|
93
|
+
if (has('haiku')) return 'Haiku-tier low-latency, high-throughput workloads';
|
|
94
|
+
break;
|
|
95
|
+
case 'OpenAI':
|
|
96
|
+
if (has('safeguard')) return 'Safety moderation & content classification';
|
|
97
|
+
if (has('gpt-oss')) return 'Open-weights GPT model for self-hosted or fine-tuned inference';
|
|
98
|
+
if (hasAny('astra', 'gpt-6')) return 'Flagship multi-step reasoning & agentic workflows (Converse API)';
|
|
99
|
+
if (has('sol')) return 'High-depth reasoning & coding (Responses API)';
|
|
100
|
+
if (has('terra')) return 'Balanced everyday production workloads (Responses API)';
|
|
101
|
+
if (has('luna')) return 'Fast, low-cost high-volume tasks (Responses API)';
|
|
102
|
+
if (hasAny('gpt-5', 'gpt-4', 'o1', 'o3', 'o4')) return 'General reasoning & coding (Responses API)';
|
|
103
|
+
break;
|
|
104
|
+
case 'DeepSeek':
|
|
105
|
+
if (has('r1')) return 'Open-weights reasoning model (chain-of-thought)';
|
|
106
|
+
if (hasAny('v3', 'deepseek')) return 'High-efficiency reasoning & code generation';
|
|
107
|
+
break;
|
|
108
|
+
case 'Meta':
|
|
109
|
+
if (has('maverick')) return 'Llama multimodal mixture-of-experts reasoning';
|
|
110
|
+
if (has('scout')) return 'Llama long-context & fast inference';
|
|
111
|
+
if (has('llama')) return 'Open-weights Llama model for general text & coding';
|
|
112
|
+
break;
|
|
113
|
+
case 'Amazon':
|
|
114
|
+
if (has('nova-premier')) return 'Complex multimodal reasoning & teacher distillation';
|
|
115
|
+
if (has('nova-pro')) return 'Multimodal reasoning, coding & agentic workflows (no FTU form required)';
|
|
116
|
+
if (has('nova-lite')) return 'Fast, low-cost multimodal tasks (no FTU form required)';
|
|
117
|
+
if (has('nova-micro')) return 'Ultra-fast, lowest-cost text workloads (no FTU form required)';
|
|
118
|
+
// Catches nova-2 and future nova generations after the known tiers.
|
|
119
|
+
if (has('nova')) return 'Multimodal reasoning, coding & cost-efficient generation';
|
|
120
|
+
if (has('titan')) return 'Amazon Titan text & embeddings model';
|
|
121
|
+
break;
|
|
122
|
+
case 'Google':
|
|
123
|
+
if (has('gemma')) return 'Open-weights multimodal & text reasoning';
|
|
124
|
+
break;
|
|
125
|
+
case 'Mistral AI':
|
|
126
|
+
if (has('voxtral')) return 'Audio & speech-to-text multimodal understanding';
|
|
127
|
+
if (has('devstral')) return 'Specialized software engineering & code reasoning';
|
|
128
|
+
if (has('large')) return 'Complex multilingual reasoning & coding';
|
|
129
|
+
if (has('codestral')) return 'Specialized code generation & completion';
|
|
130
|
+
if (has('pixtral')) return 'Multimodal vision & document understanding';
|
|
131
|
+
if (hasAny('small', 'ministral')) return 'Low-latency, cost-efficient text tasks';
|
|
132
|
+
break;
|
|
133
|
+
case 'xAI':
|
|
134
|
+
if (has('grok')) return 'Real-time reasoning, coding & tool use';
|
|
135
|
+
break;
|
|
136
|
+
case 'Moonshot AI':
|
|
137
|
+
if (has('kimi')) return 'Long-context agentic tool use & coding';
|
|
138
|
+
break;
|
|
139
|
+
case 'Cohere':
|
|
140
|
+
if (has('command')) return 'Enterprise RAG, tool use & multilingual generation';
|
|
141
|
+
if (hasAny('embed', 'rerank')) return 'Enterprise semantic search, embeddings & reranking';
|
|
142
|
+
break;
|
|
143
|
+
case 'Stability AI':
|
|
144
|
+
if (hasAny('stable', 'upscale', 'inpaint', 'outpaint')) return 'Image generation, upscaling & editing';
|
|
145
|
+
break;
|
|
146
|
+
case 'TwelveLabs':
|
|
147
|
+
if (has('pegasus')) return 'Video understanding, search & narrative extraction';
|
|
148
|
+
break;
|
|
149
|
+
case 'Writer':
|
|
150
|
+
if (has('palmyra')) return 'Enterprise-focused writing, document analysis & vision';
|
|
151
|
+
break;
|
|
152
|
+
case 'NVIDIA':
|
|
153
|
+
if (has('nemotron')) return 'High-performance enterprise reasoning & synthetic data generation';
|
|
154
|
+
break;
|
|
155
|
+
case 'Qwen':
|
|
156
|
+
if (has('qwen')) return 'Multilingual reasoning, coding & vision understanding';
|
|
157
|
+
break;
|
|
158
|
+
case 'MiniMax':
|
|
159
|
+
if (has('minimax')) return 'Long-context multilingual generation & agentic workflows';
|
|
160
|
+
break;
|
|
161
|
+
case 'Zhipu AI':
|
|
162
|
+
if (has('glm')) return 'Bilingual Chinese-English reasoning, coding & tool use';
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
return GENERIC_MODEL_HINT;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Scans a catalog for models without family-specific hint rules. Fails
|
|
171
|
+
// when any flagship (Anthropic/OpenAI) model is unmatched or when the
|
|
172
|
+
// generic-hint share exceeds maxGenericRatio.
|
|
173
|
+
export function validateCatalogHints(catalog, { maxGenericRatio = 0.15 } = {}) {
|
|
174
|
+
const unmatchedModels = [];
|
|
175
|
+
let totalModels = 0;
|
|
176
|
+
for (const group of catalog?.providers || []) {
|
|
177
|
+
const provider = normalizeProviderName(group?.provider);
|
|
178
|
+
for (const model of group?.models || []) {
|
|
179
|
+
totalModels += 1;
|
|
180
|
+
const generated = generateHintForModel(provider, model?.id, {
|
|
181
|
+
defaultModelId: catalog.defaultModelId,
|
|
182
|
+
});
|
|
183
|
+
if (model?.hint === GENERIC_MODEL_HINT || generated === GENERIC_MODEL_HINT) {
|
|
184
|
+
unmatchedModels.push({ provider, id: model?.id, name: model?.name });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const unmatchedRatio = totalModels === 0 ? 0 : unmatchedModels.length / totalModels;
|
|
189
|
+
const flagshipUnmatched = unmatchedModels.some(
|
|
190
|
+
(entry) => entry.provider === 'Anthropic' || entry.provider === 'OpenAI'
|
|
191
|
+
);
|
|
192
|
+
return {
|
|
193
|
+
ok: !flagshipUnmatched && unmatchedRatio <= maxGenericRatio,
|
|
194
|
+
totalModels,
|
|
195
|
+
unmatchedModels,
|
|
196
|
+
unmatchedRatio,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function isValidCatalog(data) {
|
|
201
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return false;
|
|
202
|
+
if (typeof data.updatedAt !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(data.updatedAt)) return false;
|
|
203
|
+
if (typeof data.defaultModelId !== 'string' || data.defaultModelId === '') return false;
|
|
204
|
+
if (!Array.isArray(data.providers) || data.providers.length === 0) return false;
|
|
205
|
+
return data.providers.every((group) => {
|
|
206
|
+
if (!group || typeof group !== 'object') return false;
|
|
207
|
+
if (typeof group.provider !== 'string' || group.provider === '') return false;
|
|
208
|
+
if (!Array.isArray(group.models) || group.models.length === 0) return false;
|
|
209
|
+
return group.models.every(
|
|
210
|
+
(model) =>
|
|
211
|
+
model &&
|
|
212
|
+
typeof model === 'object' &&
|
|
213
|
+
typeof model.id === 'string' &&
|
|
214
|
+
model.id !== '' &&
|
|
215
|
+
typeof model.name === 'string' &&
|
|
216
|
+
model.name !== ''
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Normalizes provider identifiers from inference profile IDs
|
|
222
|
+
// (`us.anthropic.claude-...`), foundation model ARNs
|
|
223
|
+
// (`...:foundation-model/anthropic.claude-...`), and plain provider names
|
|
224
|
+
// (`Anthropic`, `Mistral`) to one canonical display name.
|
|
225
|
+
export const SCOPE_ORDER = ['us', 'global', 'eu', 'apac', 'regional'];
|
|
226
|
+
const CANONICAL_SCOPE_PRIORITY = ['us', 'global', 'eu', 'apac'];
|
|
227
|
+
|
|
228
|
+
// Splits a Bedrock model ID into its cross-region scope and base model:
|
|
229
|
+
// `us.anthropic.claude-x` -> `{ scope: 'us', baseId: 'anthropic.claude-x' }`.
|
|
230
|
+
// Unprefixed (region-pinned) IDs report `regional` with the ID as its base.
|
|
231
|
+
export function parseModelScope(modelId) {
|
|
232
|
+
const match = /^(us|eu|apac|global)\.(.+)$/.exec(modelId || '');
|
|
233
|
+
if (match) return { scope: match[1], baseId: match[2] };
|
|
234
|
+
return { scope: 'regional', baseId: modelId };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Live profile names carry routing prefixes ("US ...", "GLOBAL ...") that
|
|
238
|
+
// duplicate the scope routing — strip them for a clean display name.
|
|
239
|
+
export function cleanDisplayName(name) {
|
|
240
|
+
return String(name || '').replace(/^(US|GLOBAL|Global) +/, '');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function normalizeProviderName(raw) {
|
|
244
|
+
let token = String(raw ?? '').trim();
|
|
245
|
+
const arnMarker = 'foundation-model/';
|
|
246
|
+
const arnIdx = token.indexOf(arnMarker);
|
|
247
|
+
if (arnIdx !== -1) {
|
|
248
|
+
token = token.slice(arnIdx + arnMarker.length).split('.')[0];
|
|
249
|
+
} else if (token.includes('.') && !token.includes(' ') && !token.includes('/')) {
|
|
250
|
+
const parts = token.split('.');
|
|
251
|
+
// Cross-region profile IDs prefix the provider with a geo scope
|
|
252
|
+
// (`us.anthropic...`); bare model IDs (`deepseek.v3.2`) lead with it.
|
|
253
|
+
const geoPrefixes = new Set(['us', 'eu', 'ap', 'apac', 'sa', 'ca', 'me', 'af', 'il', 'global']);
|
|
254
|
+
token = parts.length > 1 && geoPrefixes.has(parts[0].toLowerCase()) ? parts[1] : parts[0];
|
|
255
|
+
}
|
|
256
|
+
const key = token.trim().toLowerCase();
|
|
257
|
+
if (!key) return 'Other';
|
|
258
|
+
return PROVIDER_ALIASES[key] || token.trim();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Collapses cross-region inference-profile variants (`us.*`, `global.*`,
|
|
262
|
+
// unprefixed, ...) of one underlying model into a single entry per baseId.
|
|
263
|
+
// The canonical id follows us > global > eu > apac > unprefixed; surviving
|
|
264
|
+
// scopes are recorded ordered by SCOPE_ORDER (`regional` drops out when any
|
|
265
|
+
// cross-region profile exists). Order of first appearance is preserved.
|
|
266
|
+
export function dedupeProviderModels(provider, models, { defaultModelId } = {}) {
|
|
267
|
+
const byBase = new Map();
|
|
268
|
+
for (const model of models || []) {
|
|
269
|
+
const { scope, baseId } = parseModelScope(model?.id);
|
|
270
|
+
if (!byBase.has(baseId)) byBase.set(baseId, []);
|
|
271
|
+
byBase.get(baseId).push({ model, scope });
|
|
272
|
+
}
|
|
273
|
+
const deduped = [];
|
|
274
|
+
for (const [baseId, variants] of byBase) {
|
|
275
|
+
const scopes = [...new Set(variants.map((entry) => entry.scope))]
|
|
276
|
+
.sort((a, b) => SCOPE_ORDER.indexOf(a) - SCOPE_ORDER.indexOf(b));
|
|
277
|
+
const routable = scopes.some((scope) => scope !== 'regional')
|
|
278
|
+
? scopes.filter((scope) => scope !== 'regional')
|
|
279
|
+
: scopes;
|
|
280
|
+
const canonicalScope = CANONICAL_SCOPE_PRIORITY.find((scope) => routable.includes(scope));
|
|
281
|
+
const canonical = variants.find((entry) => entry.scope === canonicalScope)?.model ?? variants[0].model;
|
|
282
|
+
const entry = {
|
|
283
|
+
...canonical,
|
|
284
|
+
id: canonicalScope ? `${canonicalScope}.${baseId}` : baseId,
|
|
285
|
+
name: cleanDisplayName(canonical.name),
|
|
286
|
+
scopes: routable,
|
|
287
|
+
};
|
|
288
|
+
if (variants.some((entry) => entry.model.recommended)) {
|
|
289
|
+
entry.recommended = true;
|
|
290
|
+
entry.hint = generateHintForModel(provider, entry.id, { defaultModelId });
|
|
291
|
+
}
|
|
292
|
+
deduped.push(entry);
|
|
293
|
+
}
|
|
294
|
+
return deduped;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Finds a catalog entry by exact ID, then by base model (so an unprefixed
|
|
298
|
+
// ID still matches its canonical cross-region entry and vice versa).
|
|
299
|
+
export function findCatalogEntry(catalog, modelId) {
|
|
300
|
+
const { baseId } = parseModelScope(modelId);
|
|
301
|
+
for (const group of catalog?.providers || []) {
|
|
302
|
+
const exact = (group.models || []).find((model) => model.id === modelId);
|
|
303
|
+
if (exact) return exact;
|
|
304
|
+
}
|
|
305
|
+
for (const group of catalog?.providers || []) {
|
|
306
|
+
const byBase = (group.models || []).find((model) => parseModelScope(model.id).baseId === baseId);
|
|
307
|
+
if (byBase) return byBase;
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function findCatalogEntryById(modelId) {
|
|
313
|
+
return findCatalogEntry(loadBedrockCatalog(), modelId);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Picks the model ID variant matching the project's AWS region: eu-* regions
|
|
317
|
+
// prefer `eu`, ap-* prefer `apac`, and everything else (including us-*)
|
|
318
|
+
// prefers `us` — each falling back through `global`, `us`, then the bare
|
|
319
|
+
// baseId. Accepts a catalog entry or an ID looked up in the catalog; entries
|
|
320
|
+
// without recorded scopes (older caches) resolve from their own ID's scope.
|
|
321
|
+
export function resolveModelIdForRegion(modelEntryOrId, awsRegion = 'us-east-2') {
|
|
322
|
+
const entry = typeof modelEntryOrId === 'string' ? findCatalogEntryById(modelEntryOrId) : modelEntryOrId;
|
|
323
|
+
if (!entry || typeof entry.id !== 'string') {
|
|
324
|
+
return typeof modelEntryOrId === 'string' ? modelEntryOrId : undefined;
|
|
325
|
+
}
|
|
326
|
+
const { scope: ownScope, baseId } = parseModelScope(entry.id);
|
|
327
|
+
const scopes = Array.isArray(entry.scopes) && entry.scopes.length > 0 ? entry.scopes : [ownScope];
|
|
328
|
+
const region = awsRegion || 'us-east-2';
|
|
329
|
+
const chain = region.startsWith('eu-')
|
|
330
|
+
? ['eu', 'global', 'us']
|
|
331
|
+
: region.startsWith('ap-')
|
|
332
|
+
? ['apac', 'global', 'us']
|
|
333
|
+
: ['us', 'global'];
|
|
334
|
+
for (const scope of chain) {
|
|
335
|
+
if (scopes.includes(scope)) return `${scope}.${baseId}`;
|
|
336
|
+
}
|
|
337
|
+
return baseId;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function loadBundledCatalog() {
|
|
341
|
+
try {
|
|
342
|
+
const data = JSON.parse(fs.readFileSync(BUNDLED_CATALOG_PATH, 'utf-8'));
|
|
343
|
+
if (isValidCatalog(data)) return data;
|
|
344
|
+
} catch {
|
|
345
|
+
// Fall through to the in-memory fallback below.
|
|
346
|
+
}
|
|
347
|
+
return deepClone(FALLBACK_CATALOG);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolveCacheEnvPath() {
|
|
351
|
+
return process.env.GRADA_BEDROCK_CACHE_PATH || process.env.DEPLOY_STACK_BEDROCK_CACHE_PATH;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function resolveCachePath(options = {}) {
|
|
355
|
+
return (
|
|
356
|
+
options.cachePath ||
|
|
357
|
+
resolveCacheEnvPath() ||
|
|
358
|
+
path.join(os.homedir(), '.grada', 'bedrock-models-cache.json')
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function legacyCachePath() {
|
|
363
|
+
return path.join(os.homedir(), '.deploy-stack', 'bedrock-models-cache.json');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function loadBedrockCatalog(options = {}) {
|
|
367
|
+
const bundled = loadBundledCatalog();
|
|
368
|
+
const explicitCachePath = options.cachePath || resolveCacheEnvPath();
|
|
369
|
+
const testEnv = Boolean(process.env.VITEST || process.env.NODE_ENV === 'test');
|
|
370
|
+
if (testEnv && !explicitCachePath) return bundled;
|
|
371
|
+
// An explicit path wins outright; otherwise the new default path is
|
|
372
|
+
// tried before the legacy one. Unusable candidates fall through to
|
|
373
|
+
// the bundled catalog.
|
|
374
|
+
const candidates = explicitCachePath
|
|
375
|
+
? [explicitCachePath]
|
|
376
|
+
: [resolveCachePath(options), legacyCachePath()];
|
|
377
|
+
for (const candidate of candidates) {
|
|
378
|
+
try {
|
|
379
|
+
const cached = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
380
|
+
if (isValidCatalog(cached) && cached.updatedAt >= bundled.updatedAt) return migrateCacheShape(cached);
|
|
381
|
+
} catch {
|
|
382
|
+
// Missing, unreadable, or invalid cache: try the next candidate.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return bundled;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Caches written before cross-region dedup carry one entry per inference
|
|
389
|
+
// profile variant with no `scopes`. Collapse them in memory so listings and
|
|
390
|
+
// the picker stay deduplicated even before the next --refresh rewrites the
|
|
391
|
+
// cache file. Already-deduped caches pass through untouched.
|
|
392
|
+
function migrateCacheShape(cached) {
|
|
393
|
+
const needsMigration = cached.providers.some((group) =>
|
|
394
|
+
(group.models || []).some((model) => !Array.isArray(model?.scopes))
|
|
395
|
+
);
|
|
396
|
+
if (!needsMigration) return cached;
|
|
397
|
+
const migrated = deepClone(cached);
|
|
398
|
+
for (const group of migrated.providers) {
|
|
399
|
+
group.models = dedupeProviderModels(group.provider, group.models, {
|
|
400
|
+
defaultModelId: migrated.defaultModelId,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return migrated;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function collectLiveEntries(client) {
|
|
407
|
+
const entries = [];
|
|
408
|
+
let nextToken;
|
|
409
|
+
do {
|
|
410
|
+
const response = await client.send(
|
|
411
|
+
new ListInferenceProfilesCommand({
|
|
412
|
+
typeEquals: 'SYSTEM_DEFINED',
|
|
413
|
+
...(nextToken ? { nextToken } : {}),
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
for (const profile of response.inferenceProfileSummaries || []) {
|
|
417
|
+
if (profile?.status !== 'ACTIVE') continue;
|
|
418
|
+
const id = profile.inferenceProfileId;
|
|
419
|
+
if (typeof id !== 'string' || !MODEL_ID_PATTERN.test(id)) continue;
|
|
420
|
+
const firstArn = profile.models?.[0]?.modelArn || '';
|
|
421
|
+
entries.push({
|
|
422
|
+
id,
|
|
423
|
+
name: profile.inferenceProfileName || id,
|
|
424
|
+
provider: normalizeProviderName(firstArn || id),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
nextToken = response.nextToken;
|
|
428
|
+
} while (nextToken);
|
|
429
|
+
|
|
430
|
+
let foundationToken;
|
|
431
|
+
do {
|
|
432
|
+
const response = await client.send(
|
|
433
|
+
new ListFoundationModelsCommand({
|
|
434
|
+
byInferenceType: 'ON_DEMAND',
|
|
435
|
+
...(foundationToken ? { nextToken: foundationToken } : {}),
|
|
436
|
+
})
|
|
437
|
+
);
|
|
438
|
+
for (const model of response.modelSummaries || []) {
|
|
439
|
+
if (model?.modelLifecycle?.status !== 'ACTIVE') continue;
|
|
440
|
+
const id = model.modelId;
|
|
441
|
+
if (typeof id !== 'string' || !MODEL_ID_PATTERN.test(id)) continue;
|
|
442
|
+
entries.push({
|
|
443
|
+
id,
|
|
444
|
+
name: model.modelName || id,
|
|
445
|
+
provider: normalizeProviderName(model.providerName || id),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
foundationToken = response.nextToken;
|
|
449
|
+
} while (foundationToken);
|
|
450
|
+
|
|
451
|
+
return entries;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function refreshBedrockCatalog(options = {}) {
|
|
455
|
+
const bundled = loadBundledCatalog();
|
|
456
|
+
const region = options.region || process.env.AWS_REGION || 'us-east-2';
|
|
457
|
+
const cachePath = resolveCachePath(options);
|
|
458
|
+
const pruneMissing = options.pruneMissing === true;
|
|
459
|
+
try {
|
|
460
|
+
const client = options.bedrockClient || new BedrockClient({ region });
|
|
461
|
+
const liveEntries = await collectLiveEntries(client);
|
|
462
|
+
|
|
463
|
+
const merged = deepClone(bundled);
|
|
464
|
+
const existingIds = new Set();
|
|
465
|
+
for (const group of merged.providers) {
|
|
466
|
+
for (const model of group.models) existingIds.add(model.id);
|
|
467
|
+
}
|
|
468
|
+
const liveIds = new Set();
|
|
469
|
+
for (const entry of liveEntries) {
|
|
470
|
+
liveIds.add(entry.id);
|
|
471
|
+
if (existingIds.has(entry.id)) continue;
|
|
472
|
+
existingIds.add(entry.id);
|
|
473
|
+
let group = merged.providers.find((candidate) => candidate.provider === entry.provider);
|
|
474
|
+
if (!group) {
|
|
475
|
+
group = { provider: entry.provider, models: [] };
|
|
476
|
+
merged.providers.push(group);
|
|
477
|
+
}
|
|
478
|
+
group.models.push({
|
|
479
|
+
id: entry.id,
|
|
480
|
+
name: entry.name,
|
|
481
|
+
hint: generateHintForModel(entry.provider, entry.id, { defaultModelId: merged.defaultModelId }),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (pruneMissing && liveIds.size > 0) {
|
|
486
|
+
for (const group of merged.providers) {
|
|
487
|
+
group.models = group.models.filter(
|
|
488
|
+
(model) => liveIds.has(model.id) || model.id === merged.defaultModelId
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
merged.providers = merged.providers.filter((group) => group.models.length > 0);
|
|
492
|
+
if (!liveIds.has(merged.defaultModelId)) {
|
|
493
|
+
console.warn(`::warning file=src/data/bedrock-models.json::Default model "${merged.defaultModelId}" was not found in live ACTIVE Bedrock results and was only retained by the safety guard. Time to rotate defaultModelId.`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
for (const group of merged.providers) {
|
|
498
|
+
group.models = dedupeProviderModels(group.provider, group.models, {
|
|
499
|
+
defaultModelId: merged.defaultModelId,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
merged.updatedAt = new Date().toISOString().slice(0, 10);
|
|
504
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
505
|
+
fs.writeFileSync(cachePath, `${JSON.stringify(merged, null, 2)}\n`, 'utf-8');
|
|
506
|
+
return merged;
|
|
507
|
+
} catch (error) {
|
|
508
|
+
log.warn(`Bedrock catalog refresh failed (${error?.message || error}); using the bundled catalog.`);
|
|
509
|
+
return bundled;
|
|
510
|
+
}
|
|
511
|
+
}
|