Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions cli/commands/push/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,20 @@ interface GitProject {

async function withGitProject(test: (project: GitProject) => Promise<void>): Promise<void> {
const projectDir = await Deno.makeTempDir();
const isolatedGitConfigGlobal = await Deno.makeTempFile();
const originalGithubSha = Deno.env.get("GITHUB_SHA");
const originalGitConfigGlobal = Deno.env.get("GIT_CONFIG_GLOBAL");
const runGit = async (...args: string[]): Promise<string> => {
const result = await new Deno.Command("git", {
args,
cwd: projectDir,
clearEnv: true,
env: Object.fromEntries(
Object.entries(Deno.env.toObject()).filter(([key]) => !key.startsWith("GIT_")),
),
env: {
...Object.fromEntries(
Object.entries(Deno.env.toObject()).filter(([key]) => !key.startsWith("GIT_")),
),
GIT_CONFIG_GLOBAL: isolatedGitConfigGlobal,
},
stdout: "piped",
stderr: "piped",
}).output();
Expand All @@ -107,7 +112,10 @@ async function withGitProject(test: (project: GitProject) => Promise<void>): Pro

try {
Deno.env.delete("GITHUB_SHA");
await Deno.writeTextFile(isolatedGitConfigGlobal, "[init]\n\tdefaultBranch = main\n");
Deno.env.set("GIT_CONFIG_GLOBAL", isolatedGitConfigGlobal);
await runGit("init", "--quiet");
assertEquals(await runGit("symbolic-ref", "--short", "HEAD"), "main");
await runGit("config", "user.email", "test@veryfront.com");
await runGit("config", "user.name", "Veryfront Test");
await Deno.writeTextFile(`${projectDir}/app.ts`, "export const value = 1;\n");
Expand All @@ -117,7 +125,9 @@ async function withGitProject(test: (project: GitProject) => Promise<void>): Pro
} finally {
if (originalGithubSha === undefined) Deno.env.delete("GITHUB_SHA");
else Deno.env.set("GITHUB_SHA", originalGithubSha);
restoreEnv("GIT_CONFIG_GLOBAL", originalGitConfigGlobal);
await Deno.remove(projectDir, { recursive: true });
await Deno.remove(isolatedGitConfigGlobal);
}
}

Expand Down
2 changes: 2 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"./oauth": "./src/oauth/index.ts",
"./provider": "./src/provider/index.ts",
"./provider/shared": "./src/provider/shared/index.ts",
"./provider/openai-reasoning": "./src/provider/shared/openai-reasoning.ts",
"./provider/types": "./src/provider/types.ts",
"./fs": "./src/fs/index.ts",
"./integrations": "./src/integrations/index.ts",
Expand Down Expand Up @@ -267,6 +268,7 @@
"veryfront/discovery/runtime-modules-bootstrap": "./src/discovery/runtime-modules-bootstrap.ts",
"veryfront/observability": "./src/observability/index.ts",
"veryfront/provider/shared": "./src/provider/shared/index.ts",
"veryfront/provider/openai-reasoning": "./src/provider/shared/openai-reasoning.ts",
"veryfront/provider/types": "./src/provider/types.ts",
"veryfront/tool/schema": "./src/tool/schema/index.ts",
"veryfront/agent/composition": "./src/agent/composition/index.ts",
Expand Down
92 changes: 46 additions & 46 deletions docs/api-reference/veryfront/agent.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/api-reference/veryfront/embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const { POST, GET, DELETE } = createUploadHandler(store, {
| `ragStore` | Creates a persistent RAG store with lazy embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/rag-store.ts#L212) |
| `registerEmbeddingProvider` | Register an embedding provider factory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L25) |
| `resolveEmbeddingModel` | Resolve a "provider/model" string to an embedding runtime instance. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L116) |
| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1128) |
| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1265) |
| `vectorStore` | Creates an in-memory vector store with integrated embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/vector-store.ts#L46) |

### Types
Expand Down
70 changes: 50 additions & 20 deletions docs/api-reference/veryfront/provider.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions extensions/ext-llm-openai/src/openai-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ function _readRequestHeader(init: RequestInit | undefined, name: string): string
// ---------------------------------------------------------------------------

describe("openai-provider", () => {
it("exposes canonical model providers independently of runtime display labels", () => {
for (const createRuntime of [createOpenAIModelRuntime, createOpenAIResponsesRuntime]) {
const runtime = createRuntime({
apiKey: "test-openai-key",
name: "prod-openai",
providerName: " OpenAI ",
}, "gpt-5.4-nano");

assertEquals(runtime.provider, "prod-openai");
assertEquals(runtime.modelProvider, "openai");
}
});

it("creates an OpenAI-compatible language runtime without SDK helpers for generate", async () => {
let requestedUrl = "";
let requestedInit: RequestInit | undefined;
Expand Down
13 changes: 11 additions & 2 deletions extensions/ext-llm-openai/src/openai-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,19 @@ function getOpenAIProviderLabel(config: { name?: string }): string {
return readNonEmptyString(config.name) ?? "openai";
}

function normalizeOpenAIProviderName(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const normalized = value.trim().toLowerCase();
return normalized === "" ? undefined : normalized;
}

function getRuntimeOpenAIProviderName(config: OpenAIRuntimeConfig): string {
return readNonEmptyString(config.providerName) ?? getOpenAIProviderLabel(config);
return normalizeOpenAIProviderName(config.providerName) ??
normalizeOpenAIProviderName(config.name) ?? "openai";
}

function getLLMOpenAIProviderName(config: LLMProviderConfig): string {
return readNonEmptyString(config.providerName) ?? "openai";
return normalizeOpenAIProviderName(config.providerName) ?? "openai";
}

type OpenAICompatibleProviderKind = "openai" | "mistral" | "moonshotai";
Expand Down Expand Up @@ -1031,6 +1038,7 @@ export function createOpenAIModelRuntime(
const responseContext = { providerKind, providerLabel };
return {
provider: providerLabel,
modelProvider: providerName,
modelId,
specificationVersion: "v3",
supportedUrls: {},
Expand Down Expand Up @@ -1115,6 +1123,7 @@ export function createOpenAIResponsesRuntime(
const responseContext = { providerKind, providerLabel };
return {
provider: providerLabel,
modelProvider: providerName,
modelId,
specificationVersion: "v3",
supportedUrls: {},
Expand Down
128 changes: 14 additions & 114 deletions extensions/ext-llm-openai/src/openai-reasoning-models.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,14 @@
import type { RuntimeReasoningOption } from "veryfront/provider/types";

export type OpenAIReasoningEffort = "low" | "medium" | "high";

export type OpenAIProviderReasoningEffort = NonNullable<RuntimeReasoningOption["effort"]>;

export type OpenAIProviderReasoningOption = RuntimeReasoningOption;

export type ResolvedOpenAIReasoning = {
effort: OpenAIReasoningEffort;
source: "default" | "explicit";
};

const DEFAULT_REASONING_EFFORT: OpenAIReasoningEffort = "medium";

export function supportsDefaultReasoningParams(providerName: string): boolean {
const normalizedProvider = providerName.toLowerCase();
return normalizedProvider === "openai" || normalizedProvider === "veryfront-cloud";
}

function isGpt5ChatSnapshot(modelId: string): boolean {
return /^gpt-5-chat($|-)/.test(modelId);
}

function isGpt51(modelId: string): boolean {
return /^gpt-5\.1($|-)/.test(modelId);
}

function isReasoningCapableGpt5(modelId: string): boolean {
if (isGpt5ChatSnapshot(modelId) || isGpt51(modelId)) {
return false;
}

if (/^gpt-5(-|$)/.test(modelId)) {
return true;
}

const gpt5Version = /^gpt-5\.(\d+)(-|$)/.exec(modelId)?.[1];
return gpt5Version !== undefined && Number.parseInt(gpt5Version, 10) >= 2;
}

export function getDefaultOpenAIReasoningEffort(
modelId: string,
providerName = "openai",
): OpenAIReasoningEffort | undefined {
const normalized = modelId.toLowerCase();
if (!supportsDefaultReasoningParams(providerName)) {
return undefined;
}

if (isGpt5ChatSnapshot(normalized)) {
return undefined;
}

// GPT-5.1 defaults upstream reasoning to none unless callers opt in explicitly.
if (isGpt51(normalized)) {
return undefined;
}

if (/^o1($|-\d)/.test(normalized) || /^o[34](-|$)/.test(normalized)) {
return DEFAULT_REASONING_EFFORT;
}

if (isReasoningCapableGpt5(normalized)) {
return DEFAULT_REASONING_EFFORT;
}

return undefined;
}

export function resolveOpenAIReasoningConfig(
modelId: string,
providerName: string,
option: OpenAIProviderReasoningOption | undefined,
): ResolvedOpenAIReasoning | undefined {
if (!option) {
const effort = getDefaultOpenAIReasoningEffort(modelId, providerName);
return effort === undefined ? undefined : { effort, source: "default" };
}

if (option.enabled !== true) {
return undefined;
}

switch (option.effort) {
case "low":
return { effort: "low", source: "explicit" };
case "high":
case "max":
return { effort: "high", source: "explicit" };
case "medium":
default:
return { effort: "medium", source: "explicit" };
}
}

export function shouldRequestOpenAIReasoningSummary(
providerName: string,
reasoning: ResolvedOpenAIReasoning,
): boolean {
// Default-reasoning BYOK "openai" requests must not ask for summaries:
// unverified customer organizations get a 400 from the Responses API.
return reasoning.source === "explicit" || providerName.toLowerCase() === "veryfront-cloud";
}

export function isOpenAIReasoningModel(modelId: string, providerName = "openai"): boolean {
return getDefaultOpenAIReasoningEffort(modelId, providerName) !== undefined;
}

export function rejectsOpenAISamplingParams(modelId: string): boolean {
const normalized = modelId.toLowerCase();

return /^o[134]($|-)/.test(normalized) || isReasoningCapableGpt5(normalized);
}
export {
getDefaultOpenAIReasoningEffort,
isOpenAIReasoningModel,
rejectsOpenAISamplingParams,
resolveOpenAIReasoningConfig,
shouldRequestOpenAIReasoningSummary,
supportsDefaultReasoningParams,
} from "veryfront/provider/openai-reasoning";
export type {
OpenAIProviderReasoningEffort,
OpenAIProviderReasoningOption,
OpenAIReasoningEffort,
ResolvedOpenAIReasoning,
} from "veryfront/provider/openai-reasoning";
9 changes: 9 additions & 0 deletions src/agent/ag-ui/browser-chunk-encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import { describe, it } from "#veryfront/testing/bdd.ts";
import { createAgUiBrowserChunkEncoder } from "./browser-chunk-encoder.ts";

describe("agent/ag-ui-browser-chunk-encoder", () => {
it("exposes its state as the browser response timing anchor", () => {
const encoder = createAgUiBrowserChunkEncoder({
getRuntimeEvents: () => [],
timing: { nowMs: null, epochMs: null },
});

assertEquals(encoder.timingState, encoder.state);
});

it("merges chunk metadata into the browser finalize response", () => {
const encoder = createAgUiBrowserChunkEncoder<{
id: string;
Expand Down
3 changes: 3 additions & 0 deletions src/agent/ag-ui/browser-chunk-encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import type { AgentResponse } from "../types.ts";
/** Public API contract for AG-UI browser chunk encoder. */
export interface AgUiBrowserChunkEncoder<TChunk> {
state: AgUiBrowserEncoderState;
/** Optional timing anchor consumed by the browser response composition root. */
timingState?: AgUiBrowserEncoderState;
encode: (chunk: TChunk) => AgUiBrowserEncodedEvent[];
finalize: (response: AgentResponse | null) => AgUiBrowserEncodedEvent[];
}
Expand Down Expand Up @@ -108,6 +110,7 @@ export function createAgUiBrowserChunkEncoder<TChunk>(

return {
state: runtimeEventEncoder.state,
timingState: runtimeEventEncoder.state,
encode: (chunk) => {
mergeMetadata(runtimeEventEncoder.state.metadata, options.getMetadataFromChunk?.(chunk));
return options.getRuntimeEvents(chunk).flatMap((event) => runtimeEventEncoder.encode(event));
Expand Down
57 changes: 56 additions & 1 deletion src/agent/ag-ui/browser-encoder.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { assertEquals, assertThrows } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import {
buildAgUiBrowserFinalizeResponse,
createAgUiBrowserEncoderState,
finalizeAgUiBrowserEvents,
mapRuntimeStreamEventToAgUiBrowserEvents,
stampAgUiBrowserEventTiming,
} from "./browser-encoder.ts";

describe("agent/ag-ui-browser-encoder", () => {
Expand Down Expand Up @@ -174,6 +175,60 @@ describe("agent/ag-ui-browser-encoder", () => {
assertEquals("elapsedMs" in (events[0]?.payload ?? {}), false);
});

it("preserves valid supplied timing and rejects invalid present timing", () => {
const state = createAgUiBrowserEncoderState({
nowMs: () => Number.NaN,
epochMs: () => -1,
});
const supplied = stampAgUiBrowserEventTiming(state, [{
event: "Custom",
payload: { elapsedMs: 12.5, emittedAt: 1_786_866_357_364 },
}]);
assertEquals(supplied[0]?.payload.elapsedMs, 12.5);
assertEquals(supplied[0]?.payload.emittedAt, 1_786_866_357_364);

assertThrows(
() =>
stampAgUiBrowserEventTiming(state, [{
event: "Custom",
payload: { elapsedMs: Number.POSITIVE_INFINITY, emittedAt: 1_786_866_357_364 },
}]),
TypeError,
"elapsedMs must be a finite non-negative number",
);
assertThrows(
() =>
stampAgUiBrowserEventTiming(state, [{
event: "Custom",
payload: { elapsedMs: 0, emittedAt: -1 },
}]),
TypeError,
"emittedAt must be a non-negative integer",
);
});

it("rejects timing generated by invalid clocks", () => {
const invalidElapsed = createAgUiBrowserEncoderState({
nowMs: (() => {
let reads = 0;
return () => reads++ === 0 ? 0 : Number.NaN;
})(),
epochMs: null,
});
assertThrows(
() => mapRuntimeStreamEventToAgUiBrowserEvents(invalidElapsed, { type: "start-step" }),
TypeError,
"elapsedMs must be a finite non-negative number",
);

const invalidEpoch = createAgUiBrowserEncoderState({ nowMs: null, epochMs: () => -1 });
assertThrows(
() => mapRuntimeStreamEventToAgUiBrowserEvents(invalidEpoch, { type: "start-step" }),
TypeError,
"emittedAt must be a non-negative integer",
);
});

it("clocks the state unless a caller explicitly opts out", () => {
// Three production composition roots build this state. An opt-in clock only
// has to be missed at one of them to lose elapsedMs for every hosted run,
Expand Down
Loading