OpenAI was rejecting tool definitions with error "schema must be a JSON Schema of 'type: "object"'". This was caused by AI SDK v5 not properly converting Zod schemas to JSON Schema format. **Changes:** - Upgrade AI SDK from v5.0.104 to v6.0.0-beta.124 - Upgrade @ai-sdk/openai from v2.0.74 to v3.0.0-beta.22 - Upgrade Zod from v3.25.76 to v4.1.13 across all packages **AI SDK v6 breaking changes:** - Tool definition API: `parameters` renamed to `inputSchema` - Removed `aiTool()` wrapper - use plain object with description, inputSchema, execute - Streaming API: Use `textStream` async iterator instead of onChunk callback - Zod schemas now properly converted to JSON Schema for OpenAI **Zod v4 breaking changes:** - `z.record()` now requires two arguments: `z.record(keySchema, valueSchema)` - `z.enum()` params changed: `errorMap` removed, use `message` instead - Type system improvements require explicit type parameters - Fixed type errors in @tpmjs/env, @tpmjs/npm-client, @tpmjs/types **Files changed:** - apps/web/src/lib/ai-agent/tool-executor-agent.ts - Updated tool definition to use `inputSchema` instead of `parameters` - Removed `aiTool()` wrapper - Fixed streaming to use `textStream` iterator - packages/env/src/index.ts - Updated type constraint from `z.ZodRawShape` to `Record<string, z.ZodTypeAny>` - packages/npm-client/src/package.ts - Fixed `z.record()` calls to include both key and value schemas - Added type assertions for record indexing - packages/types/src/tpmjs.ts - Changed `errorMap` to `message` in z.enum() calls **Testing:** - ✅ Type-check passes - ✅ Production build succeeds - ✅ All routes compile correctly This fixes the tool execution error where OpenAI rejected tool schemas with invalid format. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
218 lines
5.1 KiB
TypeScript
218 lines
5.1 KiB
TypeScript
/**
|
|
* NPM Package Metadata Client
|
|
* Fetches detailed package information from NPM registry
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
|
|
const NPM_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
|
|
/**
|
|
* Schema for package version metadata
|
|
*/
|
|
const PackageVersionSchema = z.object({
|
|
name: z.string(),
|
|
version: z.string(),
|
|
description: z.string().optional(),
|
|
keywords: z.array(z.string()).optional(),
|
|
homepage: z.string().optional(),
|
|
repository: z
|
|
.union([
|
|
z.string(),
|
|
z.object({
|
|
type: z.string(),
|
|
url: z.string(),
|
|
}),
|
|
])
|
|
.optional(),
|
|
license: z.string().optional(),
|
|
author: z
|
|
.union([
|
|
z.string(),
|
|
z.object({
|
|
name: z.string(),
|
|
email: z.string().optional(),
|
|
url: z.string().optional(),
|
|
}),
|
|
])
|
|
.optional(),
|
|
maintainers: z
|
|
.array(
|
|
z.object({
|
|
name: z.string(),
|
|
email: z.string().optional(),
|
|
})
|
|
)
|
|
.optional(),
|
|
// The tpmjs field - this is what we're looking for!
|
|
tpmjs: z.unknown().optional(),
|
|
// Dist info
|
|
dist: z
|
|
.object({
|
|
shasum: z.string(),
|
|
tarball: z.string(),
|
|
})
|
|
.optional(),
|
|
// Publishing metadata
|
|
publishedAt: z.string().optional(),
|
|
});
|
|
|
|
/**
|
|
* Schema for full package metadata
|
|
*/
|
|
const PackageMetadataSchema = z.object({
|
|
name: z.string(),
|
|
'dist-tags': z.record(z.string(), z.string()),
|
|
versions: z.record(z.string(), PackageVersionSchema),
|
|
time: z.record(z.string(), z.string()),
|
|
maintainers: z
|
|
.array(
|
|
z.object({
|
|
name: z.string(),
|
|
email: z.string(),
|
|
})
|
|
)
|
|
.optional(),
|
|
description: z.string().optional(),
|
|
homepage: z.string().optional(),
|
|
keywords: z.array(z.string()).optional(),
|
|
repository: z
|
|
.union([
|
|
z.string(),
|
|
z.object({
|
|
type: z.string(),
|
|
url: z.string(),
|
|
}),
|
|
])
|
|
.optional(),
|
|
license: z.string().optional(),
|
|
readme: z.string().optional(),
|
|
});
|
|
|
|
export type PackageVersion = z.infer<typeof PackageVersionSchema>;
|
|
export type PackageMetadata = z.infer<typeof PackageMetadataSchema>;
|
|
|
|
/**
|
|
* Fetches package metadata from NPM registry
|
|
* @param packageName - The name of the package
|
|
* @returns The package metadata, or null if not found
|
|
*/
|
|
export async function fetchPackageMetadata(packageName: string): Promise<PackageMetadata | null> {
|
|
// Encode the package name to handle scoped packages (@scope/name)
|
|
const encodedName = packageName.replace('/', '%2F');
|
|
const url = `${NPM_REGISTRY_URL}/${encodedName}`;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
|
|
if (response.status === 404) {
|
|
return null;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`NPM registry error: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return PackageMetadataSchema.parse(data);
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('404')) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the latest version of a package
|
|
* @param packageName - The name of the package
|
|
* @returns The latest package version metadata, or null if not found
|
|
*/
|
|
export async function fetchLatestPackageVersion(
|
|
packageName: string
|
|
): Promise<PackageVersion | null> {
|
|
const metadata = await fetchPackageMetadata(packageName);
|
|
|
|
if (!metadata) {
|
|
return null;
|
|
}
|
|
|
|
const latestTag = metadata['dist-tags'].latest;
|
|
if (!latestTag) {
|
|
return null;
|
|
}
|
|
|
|
const version = metadata.versions[latestTag as string];
|
|
if (!version) {
|
|
return null;
|
|
}
|
|
|
|
// Add publishedAt from metadata.time
|
|
const publishedAt = metadata.time?.[latestTag as string];
|
|
|
|
return {
|
|
...version,
|
|
publishedAt,
|
|
};
|
|
}
|
|
|
|
export type PackageVersionWithReadme = PackageVersion & {
|
|
readme?: string;
|
|
topLevelKeywords?: string[];
|
|
topLevelDescription?: string;
|
|
};
|
|
|
|
/**
|
|
* Gets the latest version of a package with additional top-level metadata
|
|
* @param packageName - The name of the package
|
|
* @returns The latest package version with README and top-level metadata, or null if not found
|
|
*/
|
|
export async function fetchLatestPackageWithMetadata(
|
|
packageName: string
|
|
): Promise<PackageVersionWithReadme | null> {
|
|
const metadata = await fetchPackageMetadata(packageName);
|
|
|
|
if (!metadata) {
|
|
return null;
|
|
}
|
|
|
|
const latestTag = metadata['dist-tags'].latest;
|
|
if (!latestTag) {
|
|
return null;
|
|
}
|
|
|
|
const version = metadata.versions[latestTag as string];
|
|
if (!version) {
|
|
return null;
|
|
}
|
|
|
|
// Add publishedAt from metadata.time
|
|
const publishedAt = metadata.time?.[latestTag as string];
|
|
|
|
return {
|
|
...version,
|
|
publishedAt,
|
|
readme: metadata.readme,
|
|
topLevelKeywords: metadata.keywords,
|
|
topLevelDescription: metadata.description,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Checks if a package has a tpmjs field in its latest version
|
|
* @param packageName - The name of the package
|
|
* @returns The tpmjs field if present, null otherwise
|
|
*/
|
|
export async function fetchPackageTpmjsField(packageName: string): Promise<unknown | null> {
|
|
const latestVersion = await fetchLatestPackageVersion(packageName);
|
|
|
|
if (!latestVersion) {
|
|
return null;
|
|
}
|
|
|
|
return latestVersion.tpmjs || null;
|
|
}
|