Skip to content
Open
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
50 changes: 50 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,12 @@ export namespace ProviderTransform {
case "@ai-sdk/deepinfra":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
case "@ai-sdk/openai-compatible":
// https://v5.ai-sdk.dev/providers/community-providers/friendliai
// However, v5 support has been discontinued and only v6 is supported, so use @ai-sdk/openai-compatible
if (model.providerID === "friendli" || model.api.url?.includes("friendli")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can u share a link to this stuff?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, sure !!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

complete, it’s ok you think?

// Friendli uses chat_template_kwargs instead of reasoningEffort; variants must be defined explicitly in config
return {}
}
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))

case "@ai-sdk/azure":
Expand Down Expand Up @@ -632,6 +638,18 @@ export namespace ProviderTransform {
}

export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
if (model.providerID === "friendli" || model.api.url?.includes("friendli")) {
const { thinking: _thinking, ...cleanOptions } = options ?? {}
return {
[model.providerID]: {
...cleanOptions,
// Models released before Dec 2025 have reasoning parsing disabled for backward compatibility.
// Explicit injection required until all serverless models are upgraded.
parse_reasoning: true,
},
}
}

const key = sdkKey(model.api.npm) ?? model.providerID
return { [key]: options }
}
Expand Down Expand Up @@ -680,6 +698,38 @@ export namespace ProviderTransform {
}
*/

if (model.providerID === "friendli" || model.api.url?.includes("friendli")) {
// Friendli JSON Schema constraints: https://friendli.ai/docs/guides/structured-outputs
const SUPPORTED_FORMATS = ["uuid", "date-time", "date", "time"]
const UNSUPPORTED_STRING_KEYS = ["minLength", "maxLength"]
const UNSUPPORTED_NUMBER_KEYS = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]
const UNSUPPORTED_COMPOSITION = ["allOf", "oneOf", "not"]

const sanitize = (obj: any, parentType?: string): any => {
if (obj === null || typeof obj !== "object") return obj
if (Array.isArray(obj)) return obj.map((item) => sanitize(item, parentType))

const result: any = {}
const type = obj.type as string | undefined

for (const [key, value] of Object.entries(obj)) {
if ((type === "string" || parentType === "string") && UNSUPPORTED_STRING_KEYS.includes(key)) continue
if (type === "number" && UNSUPPORTED_NUMBER_KEYS.includes(key)) continue
if (key === "format" && typeof value === "string" && !SUPPORTED_FORMATS.includes(value)) continue
if (key === "minItems" && typeof value === "number" && value > 1) {
result[key] = 1
continue
}
if (key === "maxItems" || key === "additionalProperties") continue
if (UNSUPPORTED_COMPOSITION.includes(key)) continue

result[key] = typeof value === "object" && value !== null ? sanitize(value, type) : value
}
return result
}
schema = sanitize(schema)
}

// Convert integer enums to string enums for Google/Gemini
if (model.providerID === "google" || model.api.id.includes("gemini")) {
const sanitizeGemini = (obj: any): any => {
Expand Down