-
Notifications
You must be signed in to change notification settings - Fork 27
feat: Add AI Model Config resource and data source #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a101c75
d3e0328
691482c
f34d9d1
a927537
36b30fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| package launchdarkly | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net/http" | ||
| "strings" | ||
|
|
||
| "github.com/hashicorp/terraform-plugin-sdk/v2/diag" | ||
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
|
|
||
| ldapi "github.com/launchdarkly/api-client-go/v17" | ||
| ) | ||
|
|
||
| func baseAIModelConfigSchema(isDataSource bool) map[string]*schema.Schema { | ||
| schemaMap := map[string]*schema.Schema{ | ||
| PROJECT_KEY: { | ||
| Type: schema.TypeString, | ||
| Required: true, | ||
| ForceNew: true, | ||
| Description: addForceNewDescription("The AI model config's project key. A change in this field will force the destruction of the existing resource and the creation of a new one.", !isDataSource), | ||
| ValidateDiagFunc: validateKey(), | ||
| }, | ||
| KEY: { | ||
| Type: schema.TypeString, | ||
| Required: true, | ||
| ForceNew: true, | ||
| ValidateDiagFunc: validateKey(), | ||
| Description: addForceNewDescription("The unique key that references the AI model config. A change in this field will force the destruction of the existing resource and the creation of a new one.", !isDataSource), | ||
| }, | ||
| NAME: { | ||
| Type: schema.TypeString, | ||
| Required: !isDataSource, | ||
| Computed: isDataSource, | ||
| ForceNew: !isDataSource, | ||
| Description: addForceNewDescription("The human-friendly name for the AI model config.", !isDataSource), | ||
| }, | ||
| ID: { | ||
| Type: schema.TypeString, | ||
| Required: !isDataSource, | ||
| Computed: isDataSource, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does the user assign this or does LD? if LD assigns this it should always be computed and the force new is moot |
||
| ForceNew: !isDataSource, | ||
| Description: addForceNewDescription("Identifier for the model, for use with third party providers.", !isDataSource), | ||
| }, | ||
| MODEL_PROVIDER: { | ||
| Type: schema.TypeString, | ||
| Optional: !isDataSource, | ||
| Computed: isDataSource, | ||
| ForceNew: !isDataSource, | ||
| Description: addForceNewDescription("Provider for the model.", !isDataSource), | ||
| }, | ||
| ICON: { | ||
| Type: schema.TypeString, | ||
| Optional: !isDataSource, | ||
| Computed: isDataSource, | ||
| ForceNew: !isDataSource, | ||
| Description: addForceNewDescription("Icon for the model.", !isDataSource), | ||
| }, | ||
| PARAMS: { | ||
| Type: schema.TypeMap, | ||
| Optional: !isDataSource, | ||
| Computed: isDataSource, | ||
| ForceNew: !isDataSource, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto force new |
||
| Elem: &schema.Schema{Type: schema.TypeString}, | ||
| Description: addForceNewDescription("Model parameters as a map of key-value pairs.", !isDataSource), | ||
| }, | ||
| CUSTOM_PARAMS: { | ||
| Type: schema.TypeMap, | ||
| Optional: !isDataSource, | ||
| Computed: isDataSource, | ||
| ForceNew: !isDataSource, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto force new |
||
| Elem: &schema.Schema{Type: schema.TypeString}, | ||
| Description: addForceNewDescription("Custom model parameters as a map of key-value pairs.", !isDataSource), | ||
| }, | ||
| TAGS: tagsSchema(tagsSchemaOptions{isDataSource: isDataSource}), | ||
| COST_PER_INPUT_TOKEN: { | ||
| Type: schema.TypeFloat, | ||
| Optional: !isDataSource, | ||
| Computed: isDataSource, | ||
| ForceNew: !isDataSource, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. okay i see devin just thinks everything should force new. i'm assuming this has something to do with the no update thing, but i still think someone should look at this more closely There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the detailed review! I want to address the ForceNew concerns and explain my reasoning. The Core Issue: No PATCH Endpoint I've investigated the API client v17.2.0 and the available methods for AI Model Configs are:
There is no Is there an undocumented or planned update endpoint I should be using instead? If so, I'm happy to implement proper updates and remove ForceNew where appropriate. Addressing Your Specific Comments:
My Plan:
Please let me know if there's an update endpoint I'm missing, or if you'd prefer a different approach! |
||
| Description: addForceNewDescription("Cost per input token in USD.", !isDataSource), | ||
| }, | ||
| COST_PER_OUTPUT_TOKEN: { | ||
| Type: schema.TypeFloat, | ||
| Optional: !isDataSource, | ||
| Computed: isDataSource, | ||
| ForceNew: !isDataSource, | ||
| Description: addForceNewDescription("Cost per output token in USD.", !isDataSource), | ||
| }, | ||
| VERSION: { | ||
| Type: schema.TypeInt, | ||
| Computed: true, | ||
| Description: "Version of the AI model config.", | ||
| }, | ||
| GLOBAL: { | ||
| Type: schema.TypeBool, | ||
| Computed: true, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this user-defined or does LD auto-assign the value? if the former it should not be computed |
||
| Description: "Whether the model is global.", | ||
| }, | ||
| } | ||
|
|
||
| if !isDataSource { | ||
| schemaMap[TAGS].ForceNew = true | ||
| } | ||
|
|
||
| if isDataSource { | ||
| return removeInvalidFieldsForDataSource(schemaMap) | ||
| } | ||
|
|
||
| return schemaMap | ||
| } | ||
|
|
||
| func aiModelConfigRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}, isDataSource bool) diag.Diagnostics { | ||
| client := metaRaw.(*Client) | ||
|
|
||
| var diags diag.Diagnostics | ||
|
|
||
| projectKey := d.Get(PROJECT_KEY).(string) | ||
| key := d.Get(KEY).(string) | ||
|
|
||
| var modelConfig *ldapi.ModelConfig | ||
| var res *http.Response | ||
| var err error | ||
| err = client.withConcurrency(client.ctx, func() error { | ||
| modelConfig, res, err = client.ld.AIConfigsBetaApi.GetModelConfig(client.ctx, projectKey, key).Execute() | ||
| return err | ||
| }) | ||
|
|
||
| if isStatusNotFound(res) && !isDataSource { | ||
| diags = append(diags, diag.Diagnostic{ | ||
| Severity: diag.Warning, | ||
| Summary: "AI model config not found", | ||
| Detail: fmt.Sprintf("[WARN] AI model config %q in project %q not found, removing from state", key, projectKey), | ||
| }) | ||
| d.SetId("") | ||
| return diags | ||
| } | ||
| if err != nil { | ||
| return diag.FromErr(err) | ||
| } | ||
|
|
||
| _ = d.Set(KEY, modelConfig.Key) | ||
| _ = d.Set(NAME, modelConfig.Name) | ||
| _ = d.Set(ID, modelConfig.Id) | ||
| _ = d.Set(MODEL_PROVIDER, modelConfig.Provider) | ||
| _ = d.Set(ICON, modelConfig.Icon) | ||
| _ = d.Set(TAGS, modelConfig.Tags) | ||
| _ = d.Set(VERSION, modelConfig.Version) | ||
| _ = d.Set(GLOBAL, modelConfig.Global) | ||
|
|
||
| if modelConfig.CostPerInputToken != nil { | ||
| _ = d.Set(COST_PER_INPUT_TOKEN, *modelConfig.CostPerInputToken) | ||
| } | ||
| if modelConfig.CostPerOutputToken != nil { | ||
| _ = d.Set(COST_PER_OUTPUT_TOKEN, *modelConfig.CostPerOutputToken) | ||
| } | ||
|
|
||
| if modelConfig.Params != nil { | ||
| _ = d.Set(PARAMS, flattenParams(modelConfig.Params)) | ||
| } | ||
| if modelConfig.CustomParams != nil { | ||
| _ = d.Set(CUSTOM_PARAMS, flattenParams(modelConfig.CustomParams)) | ||
| } | ||
|
|
||
| d.SetId(projectKey + "/" + key) | ||
|
|
||
| return diags | ||
| } | ||
|
|
||
| func aiModelConfigIdToKeys(id string) (projectKey string, modelConfigKey string, err error) { | ||
| if strings.Count(id, "/") != 1 { | ||
| return "", "", fmt.Errorf("found unexpected AI model config id format: %q expected format: 'project_key/model_config_key'", id) | ||
| } | ||
| parts := strings.SplitN(id, "/", 2) | ||
| projectKey, modelConfigKey = parts[0], parts[1] | ||
| return projectKey, modelConfigKey, nil | ||
| } | ||
|
|
||
| func flattenParams(params map[string]interface{}) map[string]string { | ||
| result := make(map[string]string) | ||
| for k, v := range params { | ||
| if v != nil { | ||
| result[k] = fmt.Sprintf("%v", v) | ||
| } | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| func expandParams(params map[string]interface{}) map[string]interface{} { | ||
| if params == nil { | ||
| return nil | ||
| } | ||
| return params | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package launchdarkly | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/hashicorp/terraform-plugin-sdk/v2/diag" | ||
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
| ) | ||
|
|
||
| func dataSourceAIModelConfig() *schema.Resource { | ||
| return &schema.Resource{ | ||
| ReadContext: dataSourceAIModelConfigRead, | ||
| Schema: baseAIModelConfigSchema(true), | ||
|
|
||
| Description: `Provides a LaunchDarkly AI model config data source. | ||
|
|
||
| This data source allows you to retrieve AI model config information from your LaunchDarkly organization.`, | ||
| } | ||
| } | ||
|
|
||
| func dataSourceAIModelConfigRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { | ||
| return aiModelConfigRead(ctx, d, metaRaw, true) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you actually want to force new for resources that change their name? this will trigger a delete and then a recreation of a totally new resource with the same properties