Merge pull request 'refactor: apiKeyEnv → apiKey, store actual secret in config' (#530) from fix/528-refactor-apikey into main
This commit is contained in:
@@ -391,7 +391,7 @@ Everything else is immutable CAS content.
|
|||||||
providers:
|
providers:
|
||||||
openrouter:
|
openrouter:
|
||||||
baseUrl: "https://openrouter.ai/api/v1"
|
baseUrl: "https://openrouter.ai/api/v1"
|
||||||
apiKeyEnv: "OPENROUTER_API_KEY"
|
apiKey: "sk-..."
|
||||||
|
|
||||||
models:
|
models:
|
||||||
sonnet:
|
sonnet:
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ workflow 怎么配置和使用 model?
|
|||||||
```136:160:packages/workflow-protocol/src/types.ts
|
```136:160:packages/workflow-protocol/src/types.ts
|
||||||
export type ProviderConfig = {
|
export type ProviderConfig = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKeyEnv: string;
|
apiKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelConfig = {
|
export type ModelConfig = {
|
||||||
@@ -429,7 +429,7 @@ export type WorkflowConfig = {
|
|||||||
export function resolveModel(config: WorkflowConfig, alias: ModelAlias): ResolvedLlmProvider {
|
export function resolveModel(config: WorkflowConfig, alias: ModelAlias): ResolvedLlmProvider {
|
||||||
const modelEntry = config.models[alias];
|
const modelEntry = config.models[alias];
|
||||||
const providerEntry = config.providers[modelEntry.provider];
|
const providerEntry = config.providers[modelEntry.provider];
|
||||||
const apiKey = process.env[providerEntry.apiKeyEnv];
|
const apiKey = providerEntry.apiKey;
|
||||||
return { baseUrl: providerEntry.baseUrl, apiKey, model: modelEntry.name };
|
return { baseUrl: providerEntry.baseUrl, apiKey, model: modelEntry.name };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -280,13 +280,13 @@ threads.yaml: { "01J7K9M2XNPQR5VWBCDF8G3H4T": "8FWKR3TN5V1QA" }
|
|||||||
providers:
|
providers:
|
||||||
openai:
|
openai:
|
||||||
baseUrl: "https://api.openai.com/v1"
|
baseUrl: "https://api.openai.com/v1"
|
||||||
apiKeyEnv: "OPENAI_API_KEY"
|
apiKey: "sk-..."
|
||||||
anthropic:
|
anthropic:
|
||||||
baseUrl: "https://api.anthropic.com/v1"
|
baseUrl: "https://api.anthropic.com/v1"
|
||||||
apiKeyEnv: "ANTHROPIC_API_KEY"
|
apiKey: "sk-ant-..."
|
||||||
openrouter:
|
openrouter:
|
||||||
baseUrl: "https://openrouter.ai/api/v1"
|
baseUrl: "https://openrouter.ai/api/v1"
|
||||||
apiKeyEnv: "OPENROUTER_API_KEY"
|
apiKey: "sk-or-..."
|
||||||
|
|
||||||
models:
|
models:
|
||||||
sonnet:
|
sonnet:
|
||||||
@@ -465,7 +465,7 @@ type Scenario = string; // e.g. "extract"
|
|||||||
|
|
||||||
type ProviderConfig = {
|
type ProviderConfig = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKeyEnv: string; // env var name to read API key from
|
apiKey: string; // API key stored directly
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModelConfig = {
|
type ModelConfig = {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ uwf setup --provider openai --base-url https://api.openai.com/v1 \
|
|||||||
--api-key sk-... --model gpt-4o --agent hermes
|
--api-key sk-... --model gpt-4o --agent hermes
|
||||||
```
|
```
|
||||||
|
|
||||||
Config: `~/.uncaged/workflow/config.yaml`. API keys: `~/.uncaged/workflow/.env`.
|
Config: `~/.uncaged/workflow/config.yaml` (includes API keys).
|
||||||
|
|
||||||
### Skill
|
### Skill
|
||||||
|
|
||||||
|
|||||||
@@ -129,9 +129,8 @@ describe("cmdSetup with validation", () => {
|
|||||||
const result = await cmdSetup(setupArgs());
|
const result = await cmdSetup(setupArgs());
|
||||||
|
|
||||||
expect(result.validation).toEqual({ ok: true, value: undefined });
|
expect(result.validation).toEqual({ ok: true, value: undefined });
|
||||||
// Config files should still be written
|
// Config file should still be written
|
||||||
expect(result.configPath).toBeTruthy();
|
expect(result.configPath).toBeTruthy();
|
||||||
expect(result.envPath).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("includes validation failure — config still saved", async () => {
|
test("includes validation failure — config still saved", async () => {
|
||||||
@@ -143,8 +142,7 @@ describe("cmdSetup with validation", () => {
|
|||||||
|
|
||||||
expect(result.validation).toBeDefined();
|
expect(result.validation).toBeDefined();
|
||||||
expect((result.validation as { ok: boolean }).ok).toBe(false);
|
expect((result.validation as { ok: boolean }).ok).toBe(false);
|
||||||
// Config files should still be written despite validation failure
|
// Config file should still be written despite validation failure
|
||||||
expect(result.configPath).toBeTruthy();
|
expect(result.configPath).toBeTruthy();
|
||||||
expect(result.envPath).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,10 +85,6 @@ function getConfigPath(root: string): string {
|
|||||||
return join(root, "config.yaml");
|
return join(root, "config.yaml");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEnvPath(root: string): string {
|
|
||||||
return join(root, ".env");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load existing config.yaml or return empty structure.
|
* Load existing config.yaml or return empty structure.
|
||||||
*/
|
*/
|
||||||
@@ -106,37 +102,6 @@ function loadExistingConfig(configPath: string): Record<string, unknown> {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load existing .env as key=value map.
|
|
||||||
*/
|
|
||||||
function loadEnvFile(envPath: string): Record<string, string> {
|
|
||||||
const env: Record<string, string> = {};
|
|
||||||
try {
|
|
||||||
if (existsSync(envPath)) {
|
|
||||||
for (const line of readFileSync(envPath, "utf8").split("\n")) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
||||||
const eq = trimmed.indexOf("=");
|
|
||||||
if (eq > 0) {
|
|
||||||
env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveEnvFile(envPath: string, env: Record<string, string>): void {
|
|
||||||
const lines = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
||||||
writeFileSync(envPath, `${lines.join("\n")}\n`, "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
function apiKeyEnvName(providerName: string): string {
|
|
||||||
return `${providerName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
// Extracted helpers — _discoverAgents
|
// Extracted helpers — _discoverAgents
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -397,8 +362,7 @@ function mergeConfig(existing: Record<string, unknown>, args: SetupArgs): Record
|
|||||||
: {}
|
: {}
|
||||||
) as Record<string, unknown>;
|
) as Record<string, unknown>;
|
||||||
|
|
||||||
const envName = apiKeyEnvName(args.provider);
|
providers[args.provider] = { baseUrl: args.baseUrl, apiKey: args.apiKey };
|
||||||
providers[args.provider] = { baseUrl: args.baseUrl, apiKeyEnv: envName };
|
|
||||||
|
|
||||||
const models = (
|
const models = (
|
||||||
typeof existing.models === "object" && existing.models !== null
|
typeof existing.models === "object" && existing.models !== null
|
||||||
@@ -437,25 +401,17 @@ export async function cmdSetup(args: SetupArgs): Promise<Record<string, unknown>
|
|||||||
mkdirSync(storageRoot, { recursive: true });
|
mkdirSync(storageRoot, { recursive: true });
|
||||||
|
|
||||||
const configPath = getConfigPath(storageRoot);
|
const configPath = getConfigPath(storageRoot);
|
||||||
const envPath = getEnvPath(storageRoot);
|
|
||||||
|
|
||||||
const existing = loadExistingConfig(configPath);
|
const existing = loadExistingConfig(configPath);
|
||||||
const merged = mergeConfig(existing, args);
|
const merged = mergeConfig(existing, args);
|
||||||
|
|
||||||
writeFileSync(configPath, stringify(merged, { indent: 2 }), "utf8");
|
writeFileSync(configPath, stringify(merged, { indent: 2 }), "utf8");
|
||||||
|
|
||||||
// Write API key to .env
|
|
||||||
const envName = apiKeyEnvName(args.provider);
|
|
||||||
const envData = loadEnvFile(envPath);
|
|
||||||
envData[envName] = args.apiKey;
|
|
||||||
saveEnvFile(envPath, envData);
|
|
||||||
|
|
||||||
// Validate model connectivity
|
// Validate model connectivity
|
||||||
const validation = await validateModel(args.baseUrl, args.apiKey, args.model);
|
const validation = await validateModel(args.baseUrl, args.apiKey, args.model);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
configPath,
|
configPath,
|
||||||
envPath,
|
|
||||||
provider: args.provider,
|
provider: args.provider,
|
||||||
model: args.model,
|
model: args.model,
|
||||||
defaultAgent: merged.defaultAgent,
|
defaultAgent: merged.defaultAgent,
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ type ProviderAlias = string;
|
|||||||
type ModelAlias = string;
|
type ModelAlias = string;
|
||||||
type AgentAlias = string;
|
type AgentAlias = string;
|
||||||
|
|
||||||
type ProviderConfig = { baseUrl: string; apiKeyEnv: string };
|
type ProviderConfig = { baseUrl: string; apiKey: string };
|
||||||
type ModelConfig = {
|
type ModelConfig = {
|
||||||
provider: ProviderAlias;
|
provider: ProviderAlias;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export type Scenario = string;
|
|||||||
|
|
||||||
export type ProviderConfig = {
|
export type ProviderConfig = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKeyEnv: string;
|
apiKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelConfig = {
|
export type ModelConfig = {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { getSchema, validate } from "@uncaged/json-cas";
|
import { getSchema, validate } from "@uncaged/json-cas";
|
||||||
|
|
||||||
import type { CasRef, ModelAlias, WorkflowConfig } from "@uncaged/workflow-protocol";
|
import type { CasRef, ModelAlias, WorkflowConfig } from "@uncaged/workflow-protocol";
|
||||||
import { config as loadDotenv } from "dotenv";
|
import { createAgentStore, resolveStorageRoot } from "./storage.js";
|
||||||
import { createAgentStore, getEnvPath, resolveStorageRoot } from "./storage.js";
|
|
||||||
|
|
||||||
export type ResolvedLlmProvider = {
|
export type ResolvedLlmProvider = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@@ -38,9 +37,9 @@ export function resolveModel(config: WorkflowConfig, alias: ModelAlias): Resolve
|
|||||||
if (providerEntry === undefined) {
|
if (providerEntry === undefined) {
|
||||||
throw new Error(`unknown provider "${modelEntry.provider}" for model "${alias}"`);
|
throw new Error(`unknown provider "${modelEntry.provider}" for model "${alias}"`);
|
||||||
}
|
}
|
||||||
const apiKey = process.env[providerEntry.apiKeyEnv];
|
const apiKey = providerEntry.apiKey;
|
||||||
if (apiKey === undefined || apiKey === "") {
|
if (apiKey === undefined || apiKey === "") {
|
||||||
throw new Error(`missing API key env var: ${providerEntry.apiKeyEnv}`);
|
throw new Error(`missing API key for provider: ${modelEntry.provider}`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
baseUrl: providerEntry.baseUrl,
|
baseUrl: providerEntry.baseUrl,
|
||||||
@@ -130,7 +129,7 @@ export type ExtractResult = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Call an OpenAI-compatible LLM to extract structured output matching outputSchema.
|
* Call an OpenAI-compatible LLM to extract structured output matching outputSchema.
|
||||||
* Loads config.yaml and .env from the workflow storage root.
|
* Loads config.yaml from the workflow storage root.
|
||||||
*/
|
*/
|
||||||
export async function extract(
|
export async function extract(
|
||||||
rawOutput: string,
|
rawOutput: string,
|
||||||
@@ -138,7 +137,6 @@ export async function extract(
|
|||||||
config: WorkflowConfig,
|
config: WorkflowConfig,
|
||||||
): Promise<ExtractResult> {
|
): Promise<ExtractResult> {
|
||||||
const storageRoot = resolveStorageRoot();
|
const storageRoot = resolveStorageRoot();
|
||||||
loadDotenv({ path: getEnvPath(storageRoot) });
|
|
||||||
|
|
||||||
const { store } = await createAgentStore(storageRoot);
|
const { store } = await createAgentStore(storageRoot);
|
||||||
const schema = getSchema(store, outputSchema);
|
const schema = getSchema(store, outputSchema);
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ function normalizeProviders(raw: unknown): Record<ProviderAlias, ProviderConfig>
|
|||||||
throw new Error(`config.providers.${name} must be a mapping`);
|
throw new Error(`config.providers.${name} must be a mapping`);
|
||||||
}
|
}
|
||||||
const baseUrl = entry.baseUrl;
|
const baseUrl = entry.baseUrl;
|
||||||
const apiKeyEnv = entry.apiKeyEnv;
|
const apiKey = entry.apiKey;
|
||||||
if (typeof baseUrl !== "string" || typeof apiKeyEnv !== "string") {
|
if (typeof baseUrl !== "string" || typeof apiKey !== "string") {
|
||||||
throw new Error(`config.providers.${name} requires baseUrl and apiKeyEnv`);
|
throw new Error(`config.providers.${name} requires baseUrl and apiKey`);
|
||||||
}
|
}
|
||||||
providers[name] = { baseUrl, apiKeyEnv };
|
providers[name] = { baseUrl, apiKey };
|
||||||
}
|
}
|
||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user